---
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-ai/durable-agent
  - /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.

`DurableAgent`'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 just 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. It receives the current state and can modify the messages sent to the model:

```typescript lineNumbers
import type { ModelMessage, LanguageModel } from "ai";

interface PrepareStepInfo {
  model: string | (() => Promise<LanguageModel>);    // Current model
  stepNumber: number;                                // 0-indexed step count
  steps: StepResult[];                               // Previous step results
  messages: ModelMessage[];                          // Messages to be sent
}

interface PrepareStepResult {
  model?: string | (() => Promise<LanguageModel>);   // Override model
  messages?: ModelMessage[];                         // Override messages
}
```

## Injecting Queued Messages

Once you have a [multi-turn workflow](/docs/ai/chat-session-modeling#multi-turn-workflows), you can combine a message queue with `prepareStep` to inject messages that arrive during processing:

```typescript title="workflows/chat/index.ts" lineNumbers
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable, getWorkflowMetadata } from "workflow";
import { chatMessageHook } from "./hooks/chat-message";
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
import type { UIMessageChunk, ModelMessage } from "ai";

export async function chat(initialMessages: ModelMessage[]) {
  "use workflow";

  const { workflowRunId: runId } = getWorkflowMetadata();
  const writable = getWritable<UIMessageChunk>();
  const messageQueue: Array<{ role: "user"; content: string }> = []; // [!code highlight]

  const agent = new DurableAgent({
    model: "bedrock/claude-haiku-4-5-20251001-v1",
    instructions: FLIGHT_ASSISTANT_PROMPT,
    tools: flightBookingTools,
  });

  // Listen for messages in background (non-blocking) // [!code highlight]
  const hook = chatMessageHook.create({ token: runId }); // [!code highlight]
  hook.then(({ message }) => { // [!code highlight]
    messageQueue.push({ role: "user", content: message }); // [!code highlight]
  }); // [!code highlight]

  await agent.stream({
    messages: initialMessages,
    writable,
    prepareStep: ({ messages: currentMessages }) => { // [!code highlight]
      // Inject any queued messages before the next LLM call // [!code highlight]
      if (messageQueue.length > 0) { // [!code highlight]
        const newMessages = messageQueue.splice(0); // Drain queue // [!code highlight]
        return { // [!code highlight]
          messages: [ // [!code highlight]
            ...currentMessages, // [!code highlight]
            ...newMessages.map((m) => ({ // [!code highlight]
              role: m.role, // [!code highlight]
              content: [{ type: "text" as const, text: m.content }], // [!code highlight]
            })), // [!code highlight]
          ], // [!code highlight]
        }; // [!code highlight]
      } // [!code highlight]
      return {}; // [!code highlight]
    }, // [!code highlight]
  });
}
```

Messages sent via `chatMessageHook.resume()` accumulate in the queue and get injected before the next step, whether that's a tool call or another LLM request.

<Callout type="info">
The `prepareStep` callback receives messages in `ModelMessage[]` format (with content arrays), which is the internal format used by the AI SDK.
</Callout>

## Combining with Multi-Turn Sessions

You can also combine message queueing with the standard multi-turn pattern:

```typescript title="workflows/chat/index.ts" lineNumbers
import { DurableAgent } from "@workflow/ai/agent";
import { getWritable, getWorkflowMetadata } from "workflow";
import { chatMessageHook } from "./hooks/chat-message";
import type { UIMessageChunk, ModelMessage } from "ai";

export async function chat(initialMessages: ModelMessage[]) {
  "use workflow";

  const { workflowRunId: runId } = getWorkflowMetadata();
  const writable = getWritable<UIMessageChunk>();
  const messages: ModelMessage[] = [...initialMessages];
  const messageQueue: Array<{ role: "user"; content: string }> = [];

  const agent = new DurableAgent({ /* ... */ });
  const hook = chatMessageHook.create({ token: runId });

  while (true) {
    // Set up non-blocking listener for mid-turn messages // [!code highlight]
    let pendingMessage: string | null = null; // [!code highlight]
    hook.then(({ message }) => { // [!code highlight]
      if (message === "/done") return; // [!code highlight]
      messageQueue.push({ role: "user", content: message }); // [!code highlight]
      pendingMessage = message; // [!code highlight]
    }); // [!code highlight]

    const result = await agent.stream({
      messages,
      writable,
      preventClose: true,
      prepareStep: ({ messages: currentMessages }) => {
        // Inject queued messages during turn // [!code highlight]
        if (messageQueue.length > 0) {
          const newMessages = messageQueue.splice(0);
          return {
            messages: [
              ...currentMessages,
              ...newMessages.map((m) => ({
                role: m.role,
                content: [{ type: "text" as const, text: m.content }],
              })),
            ],
          };
        }
        return {};
      },
    });

    messages.push(...result.messages.slice(messages.length));

    // Wait for next message (either queued during turn or new) // [!code highlight]
    const { message: followUp } = pendingMessage ? { message: pendingMessage } : await hook; // [!code highlight]
    if (followUp === "/done") break;

    messages.push({ role: "user", content: followUp });
  }
}
```

## 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
- [`DurableAgent` API Reference](/docs/api-reference/workflow-ai/durable-agent) - Full API documentation
- [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
