Skip to main content
A task step runs an app-defined function as a workflow step — for work SQL and service calls can’t do, like processing a file (transcode a video, render a thumbnail, parse a PDF) or any bespoke computation. Its return value becomes the step’s results table, exactly like a service step, so later steps read it as an ordinary table.
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
If the logic is really “call an external service,” make it a service instead — services are typed, reusable, and available to the in-app agent. Keep tasks small, deterministic, and idempotent.

Defining a task

Put each task in its own file under src/server/tasks/, and use defineTask with Zod schemas. The function’s params and return type are checked against those schemas at build time:
Register tasks in a thin manifest — one line per task:
A plain object ({ 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 from params; 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 using ctx.idempotencyKey. Retries, maxRetries, and heartbeats match every other step type.

Validation