Skip to content

Programmatic submissions

Durably accept a Think turn and return before inference runs. Use submitMessages() for webhook handlers, RPC callers, and parent Workers that need a fast acknowledgement, safe retry, and later status inspection.

Declarative scheduled prompt tasks use the same durable submission path under the hood. Use getScheduledTasks() when the trigger is recurring and code-declared; use submitMessages() directly when an external caller or webhook creates one-off work. To wait for the response inline, use saveMessages() instead.

submitMessages

TypeScript
async submitMessages(
messages: UIMessage[],
options?: {
submissionId?: string;
idempotencyKey?: string;
metadata?: Record<string, unknown>;
},
): Promise<SubmitMessagesResult>

submitMessages() accepts serializable UIMessage[] values. It does not accept the function form supported by saveMessages((messages) => ...), because durable submissions persist work before execution and cannot store closures. The array must contain at least one message.

JavaScript
const submission = await this.submitMessages(
[
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Process webhook event 123" }],
},
],
{ idempotencyKey: "webhook-event-123" },
);
return Response.json({
submissionId: submission.submissionId,
status: submission.status,
accepted: submission.accepted,
});

Submission statuses

StatusMeaning
pendingAccepted and waiting for its turn
runningClaimed by the agent and executing
completedThe Think turn completed successfully
abortedThe submission was cancelled
skippedTurn state was reset before the submission ran
errorExecution failed or recovery was unsafe

Idempotent retries

Pass an idempotencyKey from your external system. Retrying with the same key returns the existing submission with accepted: false instead of inserting duplicate messages:

JavaScript
const first = await this.submitMessages(messages, {
idempotencyKey: payload.id,
});
const retry = await this.submitMessages(messages, {
idempotencyKey: payload.id,
});
console.log(first.submissionId === retry.submissionId); // true
console.log(retry.accepted); // false

If you pass both submissionId and idempotencyKey, they must identify the same submission. If they point at different existing submissions, submitMessages() throws.

Inspect, list, cancel, and delete

Use the submission APIs to inspect active work, cancel a durable submission, and clean up terminal records:

JavaScript
const current = await this.inspectSubmission(submission.submissionId);
const active = await this.listSubmissions({
status: ["pending", "running"],
});
await this.cancelSubmission(submission.submissionId, "No longer needed");
await this.deleteSubmissions({
status: ["completed", "error", "aborted"],
completedBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
});

Use cancelSubmission(submissionId) for durable cancellation across Worker and Durable Object RPC boundaries. Use AbortSignal with saveMessages() or continueLastTurn() only when the caller creates the signal inside the Durable Object that runs the turn.

Session behavior

Think stores accepted submissions in a submission ledger first. It appends submitted messages to the conversation Session only when the submission starts executing. Later accepted submissions are not visible to the model until their own turn starts, which preserves first-in, first-out turn semantics.

If the chat is cleared or turn state is reset before a pending submission runs, the submission is marked skipped.

Compared with Workflows

Use Workflows for multi-step orchestration, retries per step, long waits, external events, human approvals, or pipelines that may trigger Think as one part of a larger process. Refer to Think Workflows.