Studio previewSelf-host Studio locally.Run it

Execute and Resume Workflows

Updated 1 hour ago • September 10, 2026

Use agent.execute to invoke a registered workflow from an HTTP handler, Server Action, activity, or script. It returns one execution outcome. Use agent.resume to answer a human interrupt and continue that execution. Both use the same graph engine, registry, persistence, child calls, and telemetry as chat.

One workflow contract

import { createAgent, defineWorkflow } from "kortyx"; import { z } from "zod"; export const summaryWorkflow = defineWorkflow({ id: "summary", version: "1.0.0", inputSchema: z.object({ text: z.string() }), outputSchema: z.object({ summary: z.string() }), nodes: { summarize: { run: ({ input }: { input: { text: string } }) => ({ data: { summary: input.text.slice(0, 180) }, }), }, }, edges: [["__start__", "summarize"], ["summarize", "__end__"]], }); export const agent = createAgent({ workflows: [summaryWorkflow] });

The terminal node returns ordinary data. The workflow schemas describe its public input and output; no transport or execution mode belongs in its definition.

const result = await agent.execute({ workflow: summaryWorkflow, input: { text: "A brief to summarize." }, context: { tenantId: "tenant-1" }, }); if (result.status === "completed") { console.log(result.data.summary); // Inferred from outputSchema. }

Use the same definition registered with the agent. Both schemas are required for direct execution. Input is validated before any node starts, and completed data is parsed through the output schema. Input and output must be JSON-compatible; output must be an object. Schema transforms run on initial input and final output, not on every resume. Dynamic string workflow IDs are supported but return Record<string, unknown> rather than an inferred result.

Each execute starts a fresh run. An optional sessionId groups runs; it does not load the previous session head or make repeated calls idempotent. Omit it to create an isolated session. Chat retains its existing conversational continuation behavior.

Handle the outcome

Every result includes runId and sessionId, plus checkpointId and reported usage when available.

StatusAdditional fieldsMeaning
completeddataValidated final workflow output
suspendedinterrupt, resumeWaiting for a human response
cancelledreasonActive execution or the waiting request was cancelled
failederror.code, error.messageExecution or output validation failed

Invalid commands reject with ExecutionRequestError: for example invalid input, a mismatched workflow reference, or an invalid/stale resume handle. They do not turn an existing waiting execution into a failed run. A business rejection, such as { approved: false }, can be a successful completed result.

Resume from another endpoint

A suspended result includes the interrupt question/options and a serializable resume handle. Keep that handle private and pass it back unchanged:

const next = await agent.resume({ workflow: summaryWorkflow, resume: savedResumeHandle, response: { type: "select", ids: ["approve"] }, });

Use a workflow that actually calls useInterrupt or an interrupting child for this path. The example above illustrates the invocation shape; the simple summary workflow does not pause.

Supported responses are:

{ type: "text", text: "My answer" } { type: "select", ids: ["approve"] } { type: "cancel" }

Text responses must be nonempty. The SDK validates the response against the stored interrupt before consuming its token. Static choices must use known option IDs; a single-choice interrupt accepts one ID. Dynamic pickers with no static options retain application-owned selection validation. Built-in memory and Redis adapters claim a token atomically, so only one competing resume succeeds.

For nested children, submit the root handle. The engine restores the waiting child, returns its result to its parent, and continues. Another pause returns another suspended outcome. The workflow reference supplies result typing and must match the stored entry workflow/version.

Resume restores saved runtime context. Authorize access in the endpoint before invoking the SDK. Configure durable persistence, such as Redis, when the original caller and approval handler run in different processes or must survive a restart. The SDK does not itself schedule background work or reopen a previous HTTP/SSE connection; the next result belongs to the resume caller.

Separate HTTP routes

The Next.js API-route example provides:

PageEndpointPurpose
/POST /api/chatExisting streamed or buffered chat
/executePOST /api/executeExecute a brief workflow and inspect its outcome
/resumePOST /api/resumeResolve its child approval and see the parent result

The execute route remains small:

export async function POST(request: Request) { const body = await request.json(); const result = await agent.execute({ workflow: briefReviewWorkflow, input: body.input, }); return Response.json(result); }

The resume route uses the same server agent:

export async function POST(request: Request) { const body = await request.json(); const result = await agent.resume({ workflow: briefReviewWorkflow, resume: body.resume, response: body.response, }); return Response.json(result); }

These snippets omit application error handling. The runnable example includes it. Its UI keeps the pending handle in browser session storage for navigation between its two pages; a production approval inbox should store and authorize access on the server.

stream: false on the existing chat route still means buffered chat chunks. It does not change into this typed execution contract. parseCheckpointRequestBody and handleCheckpointRequestBody remain helpers for listing, reading, rolling back, and forking checkpoints.

Tracing, replay, and limits

Execution continues without a UI stream reader. Model calls may still use provider streaming internally. Reported token usage is cumulative for the logical execution, including child usage available at suspension. Reusing cached work does not charge it again; replayed model calls that actually run again do count. Unknown usage stays absent. Traces remain server-side, with the same run/child identities used by Studio.

A fork inherits completed work and then accumulates its own subsequent usage. Rolling back restores the checkpoint's accounting baseline. Runtime restoration cannot undo committed business side effects; use application idempotency or transactions where needed.

This API executes in-process until completion, suspension, cancellation, or failure. Whole-tree execution limits are configured in server code (see below).

Cancel active work

Pass a live AbortSignal to agent.execute, agent.resume, or agent.streamChat. In an HTTP route, forward request.signal so disconnecting the request cancels its workflow tree. createChatRouteHandler forwards it automatically; custom routes and handleChatRequestBody callers supply it explicitly.

const result = await agent.execute({ workflow: researchWorkflow, input: { topic: "Workflow cancellation" }, abortSignal: request.signal, }); if (result.status === "cancelled") { // This branch is observable when the caller is still connected. }

For a browser Stop button, use an AbortController with fetch, then call controller.abort(). The server framework must expose client disconnects through request.signal. Cancelling an SSE response body created by toSSE also cancels its active Kortyx source. A disconnected browser cannot receive the final outcome.

The signal automatically reaches child workflows, model requests, and tools. Kortyx checks it before starting subsequent nodes, retry attempts, child calls, model passes, and tools. A local reasoning/model signal is combined with the root signal; it cannot override root cancellation. Abort errors bypass node retries and ordinary tool-error feedback. Direct calls return status: "cancelled"; connected stream consumers receive a cancelled event and one root done event.

With Studio telemetry configured, run.cancelled marks the run and active child calls as cancelled. Studio presents AbortError spans as cancelled in its trace and event views. It records the outcome, not the live AbortSignal object.

Custom node I/O can cooperate using useAbortSignal:

import { useAbortSignal } from "kortyx"; async function loadDocument() { const response = await fetch("https://example.com/document", { signal: useAbortSignal(), }); return { data: { text: await response.text() } }; }

Tools receive abortSignal in their execution context. Forward it to their I/O as well. Cancellation cannot undo committed side effects or forcibly stop arbitrary JavaScript. Work that ignores the signal can delay termination until it returns; Kortyx then prevents subsequent work.

Signals are transient and never stored in checkpoints. Each resume receives its own fresh signal, including resumes of nested child interrupts and forks. A pre-aborted resume leaves the waiting handle unclaimed. Once a resume has claimed the handle and started, cancellation ends that attempt; it does not create a new human-input pause or make the consumed handle reusable. Earlier checkpoints remain available for explicit replay/fork, with the usual side-effect safeguards.

response: { type: "cancel" } remains the separate operation for declining a waiting human interrupt. Cancellation by run ID across processes is not part of this API yet.

Try /execute in the simple Next.js example: enable the 10-second delay and press Stop. /api/chat, /api/execute, and /api/resume all forward the request signal.

Bound an execution

Set limits in server code, either on the agent or on an individual execution:

const agent = createAgent({ workflows: [researchWorkflow], limits: { maxNodeExecutions: 100, maxModelPasses: 20, maxToolCalls: 40, maxChildInvocations: 10, }, }); const result = await agent.execute({ workflow: researchWorkflow, input: { topic }, limits: { maxModelPasses: 5 }, // overrides this field for this execution });

agent.streamChat(messages, { limits }) accepts the same server policy for new runs. Omitted fields inherit agent defaults; fields omitted everywhere are unlimited. Values must be positive safe integers. Never spread browser-supplied limits into these options: authorize and select policy on the server.

LimitWhat consumes one unit
maxNodeExecutionsStarting a node attempt, including retries and replay. Bounds loops that make no model calls.
maxModelPassesDispatching an invoked or streaming model request, including each tool-loop pass.
maxToolCallsStarting a tool implementation. A failed tool attempt still counts.
maxChildInvocationsStarting a new useWorkflow call. Restoring a saved child call or reusing its result does not count again.

The root, its descendants and transitionTo handoffs share one allowance. Starting a child does not reset the counters. Cached results do not consume model/tool units; actual replayed work does. Model provider SDK-internal HTTP retries are part of one model dispatch. Counts are activity limits, not token estimates or monetary ceilings; provider-reported token usage remains separate.

Kortyx checks before dispatch and suspends when the next operation would exceed a ceiling. A connected chat stream receives limit-reached with runId, limit, maximum and consumed, followed by the existing interrupt/checkpoint flow and one root done. The standard chat UI shows “Limit reached — Continue?”. Studio records run.limit_reached and shows the run and interrupted spans as paused. Cancellation continues to have its own outcome and stream event.

For a direct execution, handle the suspended outcome using the existing resume API:

if (result.status === "suspended" && result.reason === "limit_reached") { // Return the prompt and private resume handle to your authorized UI. // Call this only after the user chooses Continue: const next = await agent.resume({ workflow: researchWorkflow, resume: result.resume, response: { type: "select", ids: ["continue"] }, }); }

Continue restores the saved checkpoint with a fresh allowance using the saved server ceilings. Server code can override that next allowance with agent.resume({ ..., limits }). It does not automatically increase ceilings. Ordinary human-input resumes retain the spent allowance; limits on such a resume are not applied. Chat route limits apply to new runs; Continue uses the saved policy. Duplicate Continue requests cannot both claim the same handle.

Continuation uses the existing graph checkpoint granularity. Completed nodes and saved child results are reused. The active node, reasoning loop or partially executed tool batch can replay, repeating side effects and consuming allowance again. If the active node needs more work than its entire allowance, it can pause again at the same place; the server must choose a sufficient policy. Continue does not create checkpoints between model passes or tools and does not promise exactly-once side effects. Keep business idempotency in application code.

A fork copies checkpoint state and then spends independently. Rollback restores that checkpoint's accounting state; it does not refund money or undo side effects. There is no shared budget ledger across independent runs or forks. Token usage already recorded in the checkpoint is retained across Continue and new replayed usage is added.

Try /limits in the simple Next.js API-route example. It pauses before its second child invocation, then Continue reuses the first child's result and completes.