defineAction import. Import from "../.quickback/define-action" —
the .quickback/ directory is generated alongside your feature with a
typed helper. It infers record and Zod input, exposes the generated
Services, types c as a Hono request context, binds whereRecord and
whereTransition to the feature table, and checks the value returned by
execute against output when you declare one. Scoped
db and interactive tx are schema-aware on D1 and Neon Hyperdrive;
HTTP/WebSocket Neon keeps its dynamic database escape hatch. AppContext
also retains its open index for namespace-hydrated fields. Audit fields
(createdAt, createdBy, modifiedAt,
modifiedBy) are hard-stamped by the audit DB wrapper on every
db.insert().values() and db.update().set() — handlers don't pass
them and can't override them.
Your editor is typed too.quickback compile writes the same typed
helpers into quickback/features/<feature>/.quickback/ (plus a
quickback/tsconfig.json), so the import above resolves — and typechecks —
in the tree you actually edit, not just in the generated output. The helper
also re-exports the feature's table typed with the generated schema, so
compiler-managed columns (createdAt, modifiedBy, deletedAt,
organizationId) are visible on it without casts:
import { defineAction, applications } from "../.quickback/define-action";
Prefer this single import over the older
import { applications } from "../applications" — that path resolves to
your authored file at edit time, which has no named export and none of the
injected columns. The helper files are regenerated on every compile; keep
quickback/features/*/.quickback/ in .gitignore (new projects get this
automatically).
Before sending a compile request, the CLI loads each action in an isolated
local bundle and converts action.input plus an optional action.output with
Zod 4's z.toJSONSchema. It does not call execute; the hosted compiler never
evaluates project code. The output contract feeds OpenAPI and
Api.<feature>.actions.<action>.Output in quickback.gen.ts. Under
contract v2, every action input must produce a schema; output remains optional.
A failure names the exact
action, whether bundling or evaluation failed, and the underlying local error.
V1 can still fall back to the lower-fidelity static schema parser.
Bare imports are resolved from the generated runtime's configured
build.outputDir/node_modules first. If definitions live in quickback/ and
the generated Worker lives in src/, install runtime dependencies in src/
as usual; no project-root dependency symlink is required. Shared schema or DTO
modules may also declare top-level Drizzle selection objects. Quickback
replaces authored table exports with inert proxies during this schema-only
evaluation, so reading records.id to build a projection does not execute
application or database logic.
Parameterized standalone paths use Hono's :param syntax in authored source,
for example path: "/events/:eventId/check-in". The harvest planner converts
those segments to the OpenAPI {param} form before applying the schema, so
contract-v2 completeness uses the same route key as the emitted OpenAPI and MCP
surfaces. The same normalization applies when a record resource path contains
namespace parameters.
Portable Zod schema for the JSON success response. Generates action output types and constrains execute's result.
access
Yes
Access control (roles, record conditions, or function)
execute
Yes
Inline async arrow function (async (ctx) => { … })
path
Standalone only
Custom route path. Presence of path makes the action standalone.
method
No
HTTP method: GET, POST, PUT, PATCH, DELETE (default: POST). GET actions parse input from query params; all other methods parse from the JSON body. See GET vs POST for input.
Hint for AI tools: 'sync', 'async', or 'fire-and-forget'
allowRawSql
No
Explicit compile-time opt-in for raw SQL in execute code
unsafe
No
Unsafe raw DB mode. Object config (reason, adminOnly, crossTenant, targetScope).
transition
No
State-machine policy enforcing the from/to pair. Lives on a record-based action, or on a standalone action paired with a record: binding. See Transitions
record
Standalone only
Feature-area transition adapter binding: { table, idFrom: "path.<param>", matchScope? }. Binds + firewalls the record a transition operates on from a proven route param. See Feature-area transition adapter
bulkVariant
No
Auto-generate a POST /:resource/batch/{action} route alongside the per-record route (record-based actions). See Bulk Variant
webhook
No
Standalone only. Compile-declared inbound signature verification (standard-webhooks, hmac-sha256, or aws-sns) that runs on the raw bytes before parse/access — see Inbound webhooks
idempotency
No
'dedupe' — PUBLIC-action opt-in for the Idempotency-Key header with dedupe-only semantics (duplicate ⇒ 409 reference; the cached body is never served to a second caller). Standalone actions with a static path only (a :param path is a compile error — the allowlist matches literal request paths) and the action's access must admit PUBLIC. Authenticated routes accept the header automatically — see API contract
cms
No
Pass-through metadata blob for CMS surfaces
execute must be an async (ctx) => { … } arrow function. async function
declarations and non-async forms are rejected at parse time.
Actions can modify fields that are protected from regular CRUD operations:
// In the table file (defineTable config)guards: { protected: { status: ["advance", "reject", "hire", "withdraw"], // Only these actions can modify status }}
This allows the advance action to set status = "interview" even though the field is protected from regular PATCH requests. Direct PATCH /:id attempts to set status are rejected by the guards layer with 400 GUARD_FIELD_PROTECTED, and the field is omitted from the generated POST/PATCH body Zod schemas — clients cannot even send it past validation.
The most common action preamble — load an input id, 404 if missing, assert it
belongs to the caller's event/org — becomes declarative:
// features/agenda/actions/assignGuest.tsimport { z } from "zod";import { defineAction } from "../.quickback/define-action";export default defineAction({ description: "Assign a guest to an agenda item, optionally at a specific location.", access: { roles: ["admin", "member"] }, input: z.object({ agendaItemId: z.string(), guestId: z.string(), locationId: z.string().optional() }), refs: { agendaItemId: { table: "agendaItems", as: "agendaItem", matchScope: "event", // row.eventId === ctx.event.id notFound: { code: "ITEM_NOT_FOUND" } }, // status defaults to 404 guestId: { table: "guests", as: "guest", matchWith: { ref: "agendaItem", on: "eventId" }, // cross-input parenthood mismatch: { code: "EVENT_MISMATCH", status: 400 } }, locationId: { table: "locations", as: "location", optional: true, matchScope: "event" }, }, execute: async ({ input, agendaItem, guest, location }) => { /* … */ },});
Loads run through the scoped db before execute — the org firewall and
soft-delete filter apply by construction, so a cross-org id 404s with zero
hand-written tenancy checks. All loads batch into one Promise.all.
matchScope: "<scope>" asserts row.<scope>Id === ctx.<scope>.id and
fail-closes (500) if the scope object isn't hydrated. The explicit form
{ column, equals: "ctx.activeOrgId" } is also accepted.
matchWith compares a column against an earlier-declared ref — declaration
order defines resolution order; forward references are compile errors.
Ref keys must exist on the inline z.object input with agreeing
optionality; as names can't shadow reserved context keys. Injected rows
are the unmasked DB rows (masking remains a read-projection concern).