ExecWarden Docs

Reference

Build workflow steps that recover from committed boundaries.

Reference the executable workflow slice, recovery guarantees, idempotency limits, durable waits, hosted-agent polling, human input, and declared branches.

Current Executable Slice

Agents with workflow capability can create, run, and schedule workflows today. Schedules use five-field minute cron with optional timezone and fire through the gateway scheduler with one run per scheduled slot. Workflows can also subscribe to supported events by source/type, including configured GitHub and Slack events plus events emitted by custom webhook receivers. A workflow can be a single bounded awaited wasm-js entrypoint, or a persisted graph of committed wasm-js steps. Each run freezes the workflow definition and policy it started with. Graph steps are the durable path: each step commits before the next one starts, and JS steps can pause by returning a durable wait directive instead of sleeping inside JavaScript. Long-running agent work follows the same model: start a hosted agent run, store its run id in wait output, then poll it after the workflow resumes.

Durability Guarantees and Limits

Restart and deploy recoveryWorkflow runs, step rows, wait rows, trigger input, and the run's definition/policy snapshot are stored in the gateway database. If the same database is available after restart or deploy, startup recovery and the recovery sweep resume running and waiting graph runs.
Committed step boundaryA succeeded graph step is not re-executed on recovery. A step that was running when the process stopped can run again from its saved input, so any external side effect inside a step must be idempotent.
Durable waitsGraph JS can return delayMs or until waits. Waits are persisted as workflow timer records and are resumed by startup recovery or the recovery sweep. Waits have a maximum delay and are resumed on a sweep cadence; they are not a precise wakeup SLA.
Manual runsManual invocation creates a run immediately for an enabled workflow, stores the provided input, and executes the definition/policy snapshot captured when the run row is created.
Scheduled runsSchedules use five-field cron plus optional timezone. The scheduler runs on startup and on its configured sweep interval, uses a bounded lookback window, and uses one idempotency key per trigger and scheduled slot. It starts at most the latest due slot per sweep, so long downtime can miss older slots instead of backfilling every missed minute.
Event idempotencyPublished events are de-duped by workspace, source, and idempotency key. Matching enabled event triggers queue one delivery per event/trigger pair, and the resulting run uses an event/trigger idempotency key so delivery retries do not create duplicate runs.
Event delivery retriesThe delivery sweep runs on startup and on its configured sweep interval, using a claim window, default sweep limit, and retry backoff policy. After the configured retry attempts are exhausted, the delivery stays failed until manually retried.
Trigger timingEvent trigger matching happens when the event is published. A trigger created later does not receive old events. If a workflow or trigger is disabled or deleted before a queued delivery starts, that delivery is skipped.
Hosted-agent pollingWorkflow durability comes from the start, wait, and poll pattern. Start the hosted agent run with a stable idempotency key, persist its run id in wait output, and poll it after resume. The workflow does not keep a JS process alive while the agent works.
Retention and auditWorkflow product rows persist according to the deployment database, backup, deletion, and log-retention policy. There is no fixed public retention SLA or immutable compliance archive implied by the workflow runtime.
Not guaranteedThis is not a Temporal-grade orchestration engine, exactly-once external side-effect system, arbitrary-duration timer service, broad SaaS workflow builder, or guarantee that user-authored JS cannot perform duplicate work after a crash.

Programming Model

Graph wasm-js stepRuns bounded JavaScript in the Wasm/QuickJS sandbox. The entrypoint may use short async/await work, reads ctx.input, can emit observations with ctx.output.append(value), and returns the step value. New graph steps should accept (ctx, workflow, gateway) so they can use helpers such as workflow.route(...), workflow.next(...), workflow.sleep(...), and scoped gateway.call(...).
JS stepRuns a bounded JS/WASM step with ctx.input, ctx.output.append(value), and explicit graph control through workflow.route, workflow.next, or durable waits.
Recovery boundaryGraph workflows recover from committed step boundaries and durable wait directives.

Durable Waits

Do not keep a JavaScript loop alive while waiting for a remote condition. Run a short check, return a wait directive, and let the workflow engine persist the pause. If next is omitted, the workflow resumes the same step. If the returned value includes output, that output becomes the next input when the wait resumes.

export async function main(ctx, workflow, gateway) { const ready = await remoteCondition(ctx.input); if (ready) { return workflow.next("finish", { ready: true }); } return workflow.sleep("60s", { output: { attempt: (ctx.input.attempt ?? 0) + 1 } }); }

The raw object form still works and is what the helpers compile to:

export async function main(ctx) { const ready = await remoteCondition(ctx.input); if (ready) { return { output: { ready: true }, next: "finish" }; } return { wait: { delayMs: 60_000 }, output: { attempt: (ctx.input.attempt ?? 0) + 1 } }; }
workflow.sleep(delay, { output?, route?, next? })Pause durably. Delay can be milliseconds or a string such as 30s, 5m, 2h, or 1d.
workflow.sleepUntil(iso, { output?, route?, next? })Pause until an ISO timestamp.
workflow.next(stepId, output)Send the graph to a named step id, or pass null to stop after the current step.
{ wait: { delayMs } }Pause durably for a bounded delay, then resume the workflow.
{ wait: { until } }Pause until an ISO timestamp.
DiagnosticsWaiting runs surface workflow_wait_pending or workflow_wait_due in workflow run diagnostics.

Durable Agent Runs

Workflows should treat hosted agents as long-running jobs. Start the agent once with a stable idempotency key, return a durable wait, then poll the run after resume. This keeps the workflow recoverable and lets the next step branch from the agent result.

export async function main(ctx, workflow, gateway) { const requestId = String(ctx.input.requestId); if (!ctx.input.agentRunId) { const started = await gateway.call("gateway:agent_runs.start", { idempotencyKey: `workflow-agent-${requestId}`, title: "Review report", prompt: `Review ${ctx.input.reportName} and return launch risks.` }); if (!started.ok) throw new Error(started.error); return workflow.sleep("60s", { output: { ...ctx.input, agentRunId: started.data.run.id, attempt: 1 } }); } const fetched = await gateway.call("gateway:agent_runs.get", { runId: ctx.input.agentRunId }); if (!fetched.ok) throw new Error(fetched.error); const run = fetched.data.run; if (run.status === "succeeded") { return workflow.next("decide", { agentRunId: run.id, summary: run.result?.message }); } if (run.status === "failed" || run.status === "cancelled") { return workflow.next("handle_failure", { agentRunId: run.id, status: run.status, error: run.error }); } return workflow.sleep("60s", { output: { ...ctx.input, attempt: (ctx.input.attempt ?? 0) + 1 } }); }
CapabilityThe start/poll step needs agentRuns run access; a separate poll-only step can use agentRuns read access.
IdempotencyDerive the key from trigger input, event id, or another durable request id. Do not use randomness or current time for the same intended agent run.
ContextAgent runs started by one workflow step can be polled by a later step in the same workflow run even though each step uses a fresh short-lived key.

Human Input Pauses

Workflows can also pause when a package-backed action asks for human input. Approval-shaped pauses appear in the existing approvals surfaces and can satisfy approval policy for the selected followup action. Generic requests use the broader user-input request API for values, choices, or explicit resume signals. In both cases, the run keeps the request, answer, and followup context with the workflow history instead of requiring the agent to invent its own out-of-band checkpoint.

Declared Branches

Branching should be visible in the saved graph, not hidden inside JavaScript. Add routes to a step, then return a route label from the JS result. When a step declares routes, it must return one of those route labels; otherwise the run fails instead of silently taking an invisible default path. The Workflows page can then render the real branch structure before a run starts. Prefer workflow.route(label, output) for new graph steps; the raw { route, output } shape remains supported.

{ "id": "review", "kind": "wasm-js", "source": "export function main(ctx, workflow, gateway) { return workflow.route(ctx.input.ok ? 'approved' : 'needs_changes', ctx.input); }", "entrypoint": "main", "routes": [ { "label": "approved", "nextStepId": "publish" }, { "label": "needs_changes", "nextStepId": "revise" } ] }