Chat-as-State
Why the codebase uses conversation message history as workflow state instead of a separate state tree.
The Pattern
In a multi-step workflow, each step needs to know what previous steps produced. The naive approach is a state object passed between steps. Ship AI SaaS takes a different approach: the database is the state.
Each workflow step loads the full conversation history from the messages table, converts it to UIMessage[], and passes it to the agent. The agent reads prior tool outputs directly from the message history and infers what inputs to use.
// app/api/workflows/step/route.ts (simplified)
const dbMessages = await db.query.messages.findMany({
where: eq(messages.conversationId, conversationId),
orderBy: asc(messages.createdAt),
});
const modelMessages = await convertToModelMessages(dbMessages);
// The agent sees all prior tool outputs in its context window
await streamXxxAgent(modelMessages, team, conversationId);No state object. No serialization. No context passing between steps.
Why It Works
The Vercel AI SDK stores tool call/result pairs as structured message parts. When step N+1's agent reads the history, it sees step N's output as a tool-result message:
[user]: "Create a LinkedIn post about AI trends"
[assistant]: tool_call(generate_outline, { topic: "AI trends", platform: "LinkedIn" })
[tool]: tool_result(generate_outline, { title: "AI Trends in 2025", sections: [...] })
[assistant]: tool_call(format_post, { platform: "LinkedIn", approvedOutline: {...} })
The format_post agent doesn't need the outline explicitly passed — it reads it from the conversation history just like a human would.
Practical Implications
When building a new workflow, you do not need to wire outputs from step N to step N+1. Write each step's agent prompt to say "use the output from the previous step" and the LLM handles the rest.
Conversations Table
The conversations table has two relevant fields for this pattern:
| Column | Description |
|---|---|
niche | "general" for user-initiated chats, "workflow" for automated runs |
workflowId | FK to workflows — links this conversation to its workflow |
Each workflow run gets its own dedicated conversation. The run's conversationId is the single pointer to all its state.