Skip to main content
A workflow is a file in workflows/ that declares ordered steps. Each step runs an agent or a tool. The workflow runtime passes outputs between steps. Use workflows for multi-step work that needs ordering, branching, parallelism, retries, timeouts, or approvals. Workflow files are definitions. Starting a workflow creates a workflow run. On the Veryfront platform, that workflow run is backed by a runtime adapter so it can be queued, retried, canceled, logged, and observed in the Runs panel.

Prerequisites

  • A Veryfront project with the workflows/ directory available (see Create project).
  • Any agents or tools referenced by a step are defined in agents/ or tools/ (see Agents and Tools).
  • A provider configured for any agents the workflow uses (see Providers).

Define a workflow

Create a file in workflows/:
Steps run in order. Each step’s output is available to the next step via the workflow context. Use integrationRequirements only for explicit access that scheduled workflow runs require. Veryfront does not infer integration requirements from workflow steps, nested workflows, agents, tools, or source text.

Workflow context persistence

Workflow context must be JSON-representable. Veryfront stores suspended workflow runs as JSON, and the memory backend applies the same persistence contract as durable backends so local development and production read back the same values. Use plain JSON values in step output: strings, numbers, booleans, null, arrays, and plain objects. Values that JSON cannot encode, such as BigInt or circular references, fail persistence with a redacted context path. Veryfront keeps the fixed input and step structure and array indices, but replaces runtime object keys with <redacted> so payload identifiers do not enter logs. Some JavaScript values can be stored only after JSON rewrites them. By default, Veryfront logs a warning and persists the normalized JSON value: On edge hosts that cannot identify proxies without invoking project hooks, default mode still warns for built-ins that Veryfront can identify from native slots. It cannot safely distinguish every class instance from a proxy-backed plain object, so use strictContext when persistence must reject every value whose identity cannot be verified. Enable strictContext on a built-in backend to reject these lossy values instead of warning and normalizing:

Start a workflow

Define workflows in workflows/, then start them from the surface that owns the user or system event. Use createWorkflowClient() to register and start a workflow from server code:
Ensure every agent and tool used by the workflow exists in agents/ or tools/, then call the route:
The route returns the workflow run ID:
Inside an agent tool, start the workflow from execute:
Use handle.result() only when the caller should wait for completion. Return the runId when the workflow can continue in the background. handle.result() polls the run and resolves with the workflow output once the run completes. It throws a timeout error if the run has not reached a terminal state after 5 minutes. Set the resultWaitTimeout executor option, in milliseconds, on createWorkflowClient to change that limit.

Schedule a workflow

Use a schedule with a workflow:<workflow-id> target when a workflow must run on a schedule. See Runs for run creation and event monitoring. Each scheduled trigger creates a workflow run backed by the selected runtime adapter.

Steps

A step runs an agent or a tool:

Step options

Parallel execution

Run steps concurrently:
All three analysis steps run at the same time. The "compile" step waits for all of them to finish.

Parallel strategies

Branching

Use branch for conditional paths:
Shorthand helpers:

Human-in-the-loop

Pause a workflow until a human approves or rejects:
The workflow pauses at waitForApproval and resumes when an approver responds. If the timeout expires, the workflow fails.

Structured approval responses

Set responseSchema when the decision must carry structured data, such as a selected option or an edited value:
Submit the decision through the workflow client. The structured answer is the fifth argument to approve() and reject(), after the optional comment:
The submitted data is validated against the wait node’s responseSchema before it is persisted. A non-conformant answer is refused with an error and the approval stays pending. Validation only covers wait nodes declared in a static step list. When a workflow’s steps, or the steps of a nested loop, is a function, the node list depends on runtime state, so no schema can be resolved for the decision and the answer is accepted unvalidated. After approval, the decision lands in the workflow context under the wait node’s id, so later steps read ctx["editor-review"] as { approved, approver, comment, data, decidedAt }. The approval endpoint served by createWorkflowHandler accepts a JSON body of the shape { approved, approver, comment?, data? }. The body-level approver is compatibility input, not an identity claim. The handler replaces it with the authenticated identity returned by its server-side authorize callback, and that server-derived identity is what the workflow context persists. See Workflows: advanced for the handler routes.

Wait for events

Pause until an external event arrives, and deliver the event through the workflow client. The run resumes as soon as an event with the matching name reaches its mailbox:
Events are buffered per run, so publishing before the node parks is safe: the wait consumes the buffered event as soon as it exists. publishEvent resolves to what it did with the event: A run’s mailbox holds a bounded number of unconsumed events. Because an event is removed only when a wait takes it, none of them can be dropped safely, so a publish past the bound rejects rather than silently discarding an event some wait has not parked for yet. Reaching that bound means events are being published to a run that never consumes them. The payload lands in the workflow context under the wait node’s id, so later steps read ctx["payment-confirmed"] as { eventName, payload, receivedAt }. Read what a run is parked on with getPendingEventWaits(runId), which returns the node id, the event name, and the deadline derived from timeout. A timeout is enforced. When it elapses before the event arrives, the run fails with an error naming the node and the event it waited for. The deadline is measured from when the wait node started. Omit timeout to wait indefinitely. delay(id, duration) uses the same machinery and completes its node once the duration elapses. Canceling a run resolves its pending event waits, so a canceled run no longer reports itself as parked. Durable event waits require a backend that implements them. The built-in MemoryBackend does; RedisBackend does not currently implement the durable event-wait method group. Use hasEventWaitSupport(backend) to check a custom backend before relying on waitForEvent or delay. publishEvent is run-scoped. There is no broadcast by workflow id.

Workflow configuration

Verify it worked

createWorkflowClient() stores runs in memory, private to the client that started them. A second client, in another route file or the same file on a later request, does not see them. Verify the run from the request that started it, and add a persistent backend before reading run state from anywhere else. Add a route that starts the workflow, waits for it, and reads the finished run back through the same client:
Call it:
A working run reaches status: "completed" and exposes a nodeStates map with one completed entry per step:
If status ends in failed, inspect the matching node entry in nodeStates for the underlying error. To read run state from a different request (a status endpoint, a dashboard, or the useWorkflow hook), give every client the same persistent backend, such as RedisBackend, instead of the default in-memory one. Run state written by one in-memory client is not readable from any other.

Remove old terminal workflow runs

Use reapTerminalRuns from a periodic maintenance process to delete old completed, failed, and cancelled workflow runs. Waiting and active runs are never eligible. The cutoff is exclusive, and each call deletes at most limit runs. The sweep does not modify workflow definitions, schedules, tasks, webhooks, or prompts. Run the scheduler as one long-lived maintenance process. Before its first start, stop every workflow worker. Restart the workers only after the initial repair finishes, and keep this process running so every scheduled sweep reuses the same backend.
The backend verifies the run identity, terminal status, completion time, and mutation revision in the same operation that deletes its state. A failed run that starts retrying, or receives another state patch after the sweep reads it, is retained. A failed run with accepted retry work still queued or pending is not selected. In that case, or when Redis must resume bounded queue cleanup, hasMore is false so the loop does not immediately reselect the refreshed run. The next scheduled maintenance invocation can retry it. After deletion, reads return the existing not-found result; this retention API does not create tombstones. Redis stores a completion-time index and reads at most limit + 1 candidate records per sweep without hydrating run input, output, or context. Completion times use numeric Redis scores, so ordering stays chronological for every valid JavaScript Date. One deletion script cleans at most 100 indexed stream messages for a run, then defers final deletion if more remain. Each call requests one run-key SCAN page with COUNT 100 and reads at most 100 queue entries during repair. Redis completes both repair cycles before returning candidates. Once both finish, that backend instance queries the completed index directly and does not restart repair between deletion batches. A new backend instance performs its own bounded repair. The queue cycle uses a fixed stream high-water mark, so current queue traffic does not keep the one-time repair open forever. Before a new Redis maintenance backend starts its first retention sweep, stop every workflow worker. Run the repair until hasMore becomes false, then restart workers on the version that provides retention and keep the maintenance backend long-lived. Repeat this drain if you replace the maintenance backend. The repair requeues Redis pending deliveries that the maintenance process does not own, so running it beside active workers can duplicate their work. During repair, hasMore can be true even when one sweep examines no eligible runs.