Actions
Actions are how the outside world talks to your flow. When a client sends a message, resets state, or triggers any processing — it invokes an action. Actions are the flow's public API: named entry points with validated input and a block to execute.
Defining actions
Actions live inside defineFlow:
const myFlow = defineFlow({
kind: "my-app",
actions: {
chat: {
inputSchema: z.object({ message: z.string() }),
block: chatPipeline,
userMessage: (input) => input.message,
},
reset: {
inputSchema: z.object({}),
block: resetHandler,
},
saveArtifact: {
inputSchema: artifactInputSchema,
block: updateArtifact,
},
},
});
Each action has:
inputSchema— Zod schema that validates every incoming request. Invalid input is rejected before any block runs.block— The block (or pipeline) to execute. This is where the work happens.userMessage(optional) — Extracts a display string from the input. The framework emits it as a user-rolemessageitem before execution begins, so the conversation history shows what the user said.
Invoking actions
From the client SDK
import { createClient } from "@flow-state-dev/client";
const client = createClient({ flowKind: "my-app", userId: "user_1" });
// New session (auto-created)
await client.sendAction("chat", { message: "Hello!" });
// Existing session
await client.sendAction("chat", { message: "Hello!" }, { sessionId: "sess_123" });
From React
const session = useSession(sessionId);
await session.sendAction("chat", { message: "Hello!" });
Over HTTP
POST /api/flows/:kind/actions/:action # New session
POST /api/flows/:kind/:sessionId/actions/:action # Existing session
The server returns 202 Accepted immediately with a requestId. Execution happens asynchronously — the client connects to the SSE stream to receive results:
GET /api/flows/:kind/requests/:requestId/stream
How an action executes
Client Server
| |
|-- POST /actions/chat ------>|
| { input, userId } |-- validate input
| |-- resolve/create session
|<-- 202 { requestId } ------|
| |-- execute block (async)
|-- GET /requests/:id/stream->|
|<-- SSE events --------------| item.added, content.delta, ...
|<-- request.completed -------|
| |
Step by step:
- Validate — Input is checked against
inputSchema. Bad input returns 400. - Session — Resolved from
sessionIdor created new. - User message — If
userMessageis defined, a user-role message item is emitted to the stream. - Execute — The root block runs asynchronously. Items stream to the client as they're produced.
- Complete — Lifecycle hooks fire. The stream emits
request.completed(orrequest.failed).
Typed actions
For compile-time type safety, use createTypedClient — it generates action methods from your flow definition:
import { createTypedClient } from "@flow-state-dev/client";
const client = createTypedClient({ flow: myFlow, userId: "user_1" });
await client.actions.chat({ message: "Hello!" }); // Type-checked
await client.actions.reset({}); // Type-checked
await client.actions.invalid({ message: "Hello!" }); // TypeScript error