Developer-only. Task steps run arbitrary app code, so — like
persistToDatabase and
importFromDatabase — they are for workflows you author in src/server/workflows/. The
app’s in-app AI agent cannot compose a task step; the framework rejects any job it submits
that contains one.When to use a task step
A task is an escape hatch for work the declarative steps can’t express. Reach for the specific step first, and use a task only when none of them fit:
Use a task when you genuinely need to run code — typically over bytes or files, not rows:
- Processing an uploaded or generated file: transcode, resize, thumbnail, parse a PDF / CSV / XLSX, extract text, convert formats
- CPU-bound computation, or a library that operates on file contents
- Anything that must open a blob, do real work, and write a new blob
Defining a task
Put each task in its own file undersrc/server/tasks/, and use defineTask with Zod schemas.
The function’s params and return type are checked against those schemas at build time:
{ inputSchema, outputSchema, run }) is also accepted, but defineTask is
preferred — it is what gives the compile-time param and return guarantee.
Referencing a task in a workflow
Configuration
The return value is written to
outputTable / outputSchema like a service step: an array of
rows, a single row object, or a { data: [...] }-shaped value.
The task context
run(params, ctx) receives a context assembled by the worker:
Org is ordinary app data — pass it in via a
param (for example {{var.orgId}}) like any other value.
The canonical pipeline: import → task → persist
A task takes its input fromparams; to act on a prior step’s rows, wrap it in
forEach and reference the row’s columns with
{{item.X}}. This is the common file-processing shape:
Execution semantics
Execution is at-least-once. The results table is overwritten on retry (idempotent by construction); make any side effects idempotent yourself usingctx.idempotencyKey. Retries,
maxRetries, and heartbeats match every other step type.

