workflow
Version:
Workflow SDK - Build durable, resilient, and observable workflows
139 lines (112 loc) • 6.02 kB
text/mdx
---
title: Queueing User Messages
description: Inject messages during an agent's turn, before tool calls complete or while the model is reasoning.
type: guide
summary: Inject user messages mid-turn using the `prepareStep` callback to influence the agent's next step.
prerequisites:
- /docs/ai
related:
- /docs/ai/chat-session-modeling
- /docs/api-reference/workflow/define-hook
---
When using [multi-turn workflows](/docs/ai/chat-session-modeling#multi-turn-workflows), messages typically arrive between agent turns. The workflow waits at a hook, receives a message, then starts a new turn. But sometimes you need to inject messages *during* an agent's turn, before tool calls complete or while the model is reasoning.
`WorkflowAgent`'s `prepareStep` callback enables this by running before each step in the agent loop, giving you a chance to inject queued messages into the conversation. `prepareStep` also allows you to modify the model choice and existing messages mid-turn, see AI SDK's [prepareStep callback](https://ai-sdk.dev/docs/agents/loop-control#prepare-step) for more details.
## When to use this
Message queueing is useful when:
- Users send follow-up messages while the agent is still searching for flights or processing bookings
- External systems need to inject context mid-turn (e.g., a flight status webhook fires during processing)
- You want messages to influence the agent's next step rather than waiting for the current turn to complete
<Callout type="info">
If you need basic multi-turn conversations where messages arrive between turns, see [Chat Session Modeling](/docs/ai/chat-session-modeling). This guide covers the more advanced case of injecting messages *during* turns.
</Callout>
## The `prepareStep` callback
The `prepareStep` callback runs before each step in the agent loop. Use WorkflowAgent's exported types rather than redeclaring its normalized provider-prompt contract:
```typescript lineNumbers
import type {
PrepareStepInfo,
PrepareStepResult,
} from "@ai-sdk/workflow";
const prepareStep = (
{ messages }: PrepareStepInfo
): PrepareStepResult => ({ messages });
```
`PrepareStepInfo.messages` is a normalized `LanguageModelV4Prompt`, not the application-level `ModelMessage[]` accepted by `WorkflowAgent.stream()`.
## Queueing messages during and between turns
Use one async Hook consumer and one FIFO. `prepareStep` atomically drains messages that arrived during a model turn; messages that arrive after the final model step become input to the next turn. Each Hook payload therefore has exactly one ownership path.
```typescript title="workflows/chat/index.ts" lineNumbers
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
import { getWritable, getWorkflowMetadata } from "workflow";
import { chatMessageHook } from "./hooks/chat-message";
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
import type { ModelMessage } from "ai";
export async function chat(initialMessages: ModelMessage[]) {
"use workflow";
const { workflowRunId: runId } = getWorkflowMetadata();
const writable = getWritable<ModelCallStreamPart>();
let messages: ModelMessage[] = [...initialMessages];
const messageQueue: Array<{ role: "user"; content: string }> = []; // [!code highlight]
let stopped = false;
let notifyMessage: (() => void) | undefined;
const agent = new WorkflowAgent({
model: "spacexai/grok-4.6",
instructions: FLIGHT_ASSISTANT_PROMPT,
tools: flightBookingTools,
});
const hook = chatMessageHook.create({ token: runId }); // [!code highlight]
// This is the only code path that consumes Hook payloads. // [!code highlight]
const consumeMessages = (async () => { // [!code highlight]
for await (const { message } of hook) { // [!code highlight]
if (message === "/done") { // [!code highlight]
stopped = true; // [!code highlight]
notifyMessage?.(); // [!code highlight]
break; // [!code highlight]
} // [!code highlight]
messageQueue.push({ role: "user", content: message }); // [!code highlight]
notifyMessage?.(); // [!code highlight]
notifyMessage = undefined; // [!code highlight]
} // [!code highlight]
})(); // [!code highlight]
const waitForMessage = async () => {
while (messageQueue.length === 0 && !stopped) {
await new Promise<void>((resolve) => {
notifyMessage = resolve;
});
}
};
while (!stopped) {
const result = await agent.stream({
messages,
writable,
preventClose: true,
sendFinish: false,
prepareStep: ({ messages: currentMessages }) => {
const queued = messageQueue.splice(0); // Atomic drain // [!code highlight]
if (queued.length === 0) return {};
return {
messages: [
...currentMessages,
...queued.map(({ role, content }) => ({
role,
content: [{ type: "text" as const, text: content }],
})),
],
};
},
});
messages = result.messages;
if (stopped) break;
await waitForMessage(); // [!code highlight]
if (stopped) break;
// Anything not consumed by prepareStep arrived after the final model step.
messages = [...messages, ...messageQueue.splice(0)]; // [!code highlight]
}
await consumeMessages;
return { messages };
}
```
Messages sent via `chatMessageHook.resume()` accumulate until either `prepareStep` or the between-turn branch drains the FIFO. Send `/done` to stop the consumer and let the workflow return.
## Related documentation
- [Chat Session Modeling](/docs/ai/chat-session-modeling) - Single-turn vs multi-turn patterns
- [Building Durable AI Agents](/docs/ai) - Complete guide to creating durable agents
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options