Prerequisites
- A working workflow defined and runnable per Workflows.
- For React hooks: a client page that can render React components and an API
route that matches the hook’s
apiBase.
Loops
Repeat steps based on conditions:loop checks the condition before each iteration. doWhile runs the body once,
then repeats until its condition is true. times runs a fixed number of
iterations. map runs the processor once per item in an array.
Blob storage
For large workflow artifacts (uploaded files, generated reports, intermediate datasets), configureblobStorage on the executor with a host-provided storage
adapter. The public workflow export exposes the executor integration point.
Storage implementations come from the host runtime: typical hosts wire S3, GCS,
or Vercel Blob behind this adapter.
Without blobStorage, large values still flow through step inputs and outputs
in memory, which becomes the bottleneck once individual artifacts exceed a few
hundred kilobytes.
React hooks
Track workflow progress from the client when your app exposes workflow API routes that match the hook’sapiBase:
useWorkflowStart posts to ${apiBase}/${workflowId}/start. useWorkflow
subscribes to ${apiBase}/runs/${runId} and keeps status and nodeStates in
sync with the server’s run state.
Serve the hook routes
Nothing serves those paths by default. MountcreateWorkflowHandler on a
catch-all route to answer all of them at once:
Before mounting this route, provide lib/auth.ts through your application’s
server-side authentication layer. Its getSession(request) function must verify
a signed, HttpOnly, same-origin session cookie and return
Promise<{ user: { id: string } } | null>. Do not decode an unsigned cookie or
accept a browser-supplied user ID header as authentication.
authorize callback must validate the request with your server-side session
implementation and return the authenticated user’s stable ID. It can also deny
access to individual routes. Returning null denies the request. Veryfront uses
the returned ID for approval decisions and does not trust the approver name in
the browser request. Non-canonical route encodings are rejected before this
callback runs, so route-specific policies cannot authorize a different path
from the operation the handler dispatches.
The built-in handler does not apply per-run ownership filtering. Only authorize
an identity when it can read every run summary visible to the supplied client.
The approval-by-ID route returns the approval payload, so the identity must also
be allowed to read and decide those approvals. Use separate clients or separate
route authorization when users have different run visibility.
The handler covers every path the hooks call:
Mounting somewhere else means telling both sides. Pass
basePath to the handler
and the matching apiBase to every hook.
Understand run summaries
GET /runs, GET /runs/{runId}, and the first SSE snapshot frame return the
same WorkflowRunSummary shape. The built-in handler constructs this response
from an allowlist:
useApproval to fetch an approval payload.
Errors and approval request messages remain visible because the hooks and SSE
events use them for operations. Do not place secrets, tokens, customer payloads,
or private model output in developer-authored errors or approval messages. The
summary is data-minimized, not guaranteed secret-free.
WorkflowClient remains a trusted server-side API and returns the durable full
run state. Do not serialize its run values directly to a browser. If existing
browser code reads run.input, run.output, run.context, node payloads, or
approval payloads from useWorkflow, useWorkflowList, or the built-in run
routes, move that read to a separately authorized server endpoint. Select only
the fields the application needs. Use useApproval for approval payloads.
Call the hooks across origins
A cross-originapiBase needs CORS on both the preflight and the actual
response. Configure the global policy with only the application origins you
control and every request header the hooks send:
createWorkflowHandler owns GET and POST. Export those handlers normally;
Veryfront uses security.cors for automatic preflight and for the actual
responses:
headers option when the
workflow origin uses bearer authentication. Set credentials: "include" only
for a credentialed cookie session; that mode requires the exact-origin
Access-Control-Allow-Origin and Access-Control-Allow-Credentials: true
settings shown above. The global preflight policy must allow Content-Type,
Authorization, and any configured CSRF header the client sends.
Stream run events
Use the SSE route when a dashboard, operator, or CI client needs durable progress without polling the run endpoint:EventSource example assumes the handler authorizes a same-origin
cookie session, which the browser sends automatically. For a cross-origin
cookie session, call observeWorkflowRun(runId, { withCredentials: true }) and
allow credentialed CORS on the workflow origin. Native EventSource cannot set
an Authorization header. Bearer-token clients must use a fetch-based SSE
client and pass the same authorization header used by the workflow hooks.
The first frame is normally snapshot, using the same run summary as
GET /runs/{runId}. When the stored run cannot be serialized, the stream opens
with a single error frame instead and closes; reconnecting re-reads the same
stored run, so that error is marked not retryable. Later frames use these
shapes:
A run that parks on
waitForApproval reports run.status with waiting
first, then approval.pending once the approval is persisted. The event names
the blocking approval directly, so a subscriber can render or decide it without
fetching the run’s approvals and racing the approval write. Approval payloads
are not part of the stream; fetch the approval by id when the decision needs
them.
Top-level sequential nodes persist running before their side effect starts and
persist their settled state before dependents execute. Parallel nodes start as a
batch and settle after the batch joins. Top-level composites report their own
boundaries; synthetic child graphs do not replace the durable root state.
The same boundaries keep currentNodes on the run current. While a run is
running it names the batch in flight, so a run that stops making progress
shows which step it is on from its persisted state alone. While the run is
waiting it names the node the run is parked on, which can be a child of a
composite. It is empty once the run completes. A failed run keeps the nodes in
its terminal batch that failed or were still running. A cancelled run keeps the
last recorded value, so both terminal states still name where execution stopped.
A terminal snapshot closes immediately. A terminal run.status frame is the last
transition frame. Cancelling the response or aborting the request releases the
backend observation. An observation failure sends the sanitized error frame and
then closes.
The handler admits at most 64 active event streams by default, and at most 8
streams for one authorized identity. These limits are local to the handler
instance. Configure them when mounting the handler if the deployment needs a
different budget:
429 with a JSON message
before opening a backend observation. Treat this as a transient admission
failure and retry with backoff after an existing stream closes. In a deployment
with multiple handler processes, each process enforces its own limits.
Native EventSource reconnects automatically when a transport disconnects or a
successful SSE response reaches EOF. The helper calls close() for terminal runs
to avoid reconnecting to an already-finished run. A transport failure dispatches
a plain Event, which keeps the native reconnect behavior. The server’s named
error frame dispatches a MessageEvent; the helper closes it so the caller can
decide when to retry from a fresh snapshot.
A new connection receives a fresh snapshot and future transitions. It does not
replay transitions that are already represented by that snapshot. A missing run
returns 404. A custom backend that does not implement atomic run observation
returns 501.
Verify it worked
For loops, run the workflow with an input that triggers the loop condition (a low review score, an unfinished check, an array of URLs). The dev-server log shows the loop body executing once per iteration. The final run status reachescompleted after the condition flips.
For blob storage, configure an adapter, run a workflow that writes a large
artifact, and ensure the storage backend received it. The step output should
reference a blob handle rather than the inline payload.
For React hooks, mount the handler as shown above, render the dashboard
component, select Run Pipeline, and ensure the status string moves through
running to completed while individual nodeStates entries update. A status
that never leaves its initial value usually means the hook routes are not
mounted, so every poll is answering 404.