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/ortools/(see Agents and Tools). - A provider configured for any agents the workflow uses (see Providers).
Define a workflow
Create a file inworkflows/:
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 asBigInt 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 inworkflows/, then start them from the surface that owns the user or system event.
Use
createWorkflowClient() to register and start a workflow from server code:
agent and tool used by the workflow exists in agents/ or tools/, then call the route:
execute:
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 aworkflow:<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:"compile" step waits for all of them to finish.
Parallel strategies
Branching
Usebranch for conditional paths:
Human-in-the-loop
Pause a workflow until a human approves or rejects:waitForApproval and resumes when an approver responds. If the timeout expires, the workflow fails.
Structured approval responses
SetresponseSchema when the decision must carry structured data, such as a
selected option or an edited value:
approve() and reject(), after the optional comment:
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: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:
status: "completed" and exposes a nodeStates map with one completed entry per step:
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
UsereapTerminalRuns 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.
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.