Workflows
September 13, 2026 · View on GitHub
Kody exposes Cloudflare Workflows through kody:runtime in every server-side
runtime context: execute, package jobs, package subscriptions, package
exports, and package apps.
Use workflows instead of plain execute for durable batch sweeps, migrations,
polling loops, retryable steps, or work that may run longer than execute's
timeout (~90s). Workflow-invoked package exports and inline workflow code get a
longer sandbox budget (~4.5 minutes, under the Cloudflare Workflow step timeout)
and run without the package-invocation idempotency ledger, so a step retry
re-executes instead of replaying a cached timeout. Outbound fetch in that
sandbox is capped ~30s under the same budget (~4 minutes), so a single slow
upstream can finish without the execute-oriented 60s fetch deadline. The initial
execute call should submit one workflows.create; inspect that workflow later
with workflowRunList, or cancel it with workflowRunCancel. Check-heavy admin
steps such as fleet apply, dry-run, and revert take one page per workflow
sandbox; when nextCursor is set, create another workflow with that runId and
cursor instead of looping in the same run.
import { workflows } from 'kody:runtime'
export default async function main(params) {
return await workflows.create({
code: 'export default async function main(p) { return { ok: true, p } }',
params: { greeting: params.greeting ?? 'hello' },
})
}
Check status from a later MCP call:
import { kody } from 'kody:runtime'
export default async function main() {
return await kody.workflowRunList({ limit: 10 })
}
workflows.create accepts one durable workflow request with two source shapes:
code: a complete ESM module string with a default export. Kody runs it later through the same module loader used byexecute, including statickody:@imports andimport(specifier)when the package name is data.exportName: a saved-package export to invoke later. In package runtime code, Kody resolvespackageIdfrompackageContext. Outside a package runtime, passpackageIdexplicitly.
Both shapes accept:
workflowName: optional display name. When omitted, inline workflows fall back toinline-codeand package workflows fall back to the export path. On/account/workflowsand Activity, a displayedinline-codename shows the idempotency key as a subtitle so those runs stay distinguishable.runAt: optional ISO date-time string orDate; defaults to nowidempotencyKey: optional caller-chosen dedupe key; omitted keys create a fresh runparams: optional JSON object passed to the workflow body
Calling create again with the same explicit idempotencyKey and matching
workflow identity for the same user returns the existing workflow instead of
starting a duplicate. Choose keys that include the logical job identity, for
example storage-sweep:2026-05-08. Kody enforces a finite per-user concurrent
workflow limit from the account plan (see
Plans and pricing); if the cap is reached,
workflows.create returns a clear quota error.
Use workflowRunList to inspect recent workflow runs and statuses, and
workflowRunCancel to stop a run by id.
Cancelling workflow runs
workflowRunCancel({ id }) cancels one workflow run by id. Run ids look like
dynwf-… for inline runs and pkgwf-… for package runs; get them from
workflows.create output or workflowRunList. The call terminates the
underlying Cloudflare Workflow instance and marks the run cancelled in
workflowRunList.
You can only cancel your own runs. Unknown ids or another user's id return a "not found" error.
Cancelling an already-finished run (complete, errored, terminated, or
cancelled) is a safe no-op: the response has cancelled: false and
already_terminal: true with the run's terminal status. Cancelling is
idempotent. If the run finishes in the moment you cancel it, the cancel reports
the run's actual terminal status instead of pretending it was cancelled. In rare
races a cancelled run can instead surface as terminated (the engine's own
terminal status) — treat cancelled and terminated both as "the run was
stopped".
A cancelled run keeps single-flighting its idempotency key, exactly like a
complete or errored run — calling workflows.create again with the same key
returns the cancelled run instead of starting a new one. To genuinely re-run,
pick a new idempotency key. This prevents a cancelled self-rescheduling chain
from being accidentally revived by a retry that reuses old keys.
Cancelling one run does not un-schedule runs it already created. Each queued run
is an independent workflow instance — use workflowRunList to find every queued
run in the chain and cancel each one by id. A run that is mid-execution may
still create its successor before termination lands, so list again after
cancelling to catch stragglers.
import { kody } from 'kody:runtime'
export default async function main() {
return await kody.workflowRunCancel({ id: 'dynwf-abc123' })
}
Package export example
import { workflows } from 'kody:runtime'
export default async function main() {
return await workflows.create({
packageId: 'pkg_123',
exportName: './workflow-run-event',
runAt: new Date(Date.now() + 60_000).toISOString(),
idempotencyKey: 'sync-account-123',
params: { accountId: 'account-123' },
})
}
Saved package jobs and subscriptions call the same workflows.create helper.
Workflow entrypoints are not declared under kody.workflows; the hub resolves
any package export by name at runtime, so calling
workflows.create({ exportName: './workflow-run-event' }) from a package
runtime context is enough.