The `Next` Hook runs after the LLM responds and any tool calls complete. Use it to process the result, persist data, send custom messages, or drive the agent into another execution round.
## Minimal Implementation
```typescript
// assistants/my-assistant/src/index.ts
import { agent, Process } from "@yao/runtime";
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
return null; // default: surface the LLM response as-is
}
```
## The Payload
```typescript
interface Payload {
messages: agent.Message[]; // full messages sent to LLM (including history)
completion?: {
content: string | ContentPart[]; // LLM text response, or multimodal parts
tool_calls?: ToolCall[]; // tool calls requested by LLM
reasoning_content?: string; // reasoning trace (o1 / DeepSeek R1 models)
finish_reason?: string; // "stop" | "length" | "tool_calls" | "content_filter"
usage?: UsageInfo; // token usage
};
tools?: ToolCallResponse[]; // tool call execution results
error?: string; // set if LLM or tool execution failed
}
```
Each `ToolCallResponse` in `tools`:
```typescript
interface ToolCallResponse {
toolcall_id: string; // unique ID for this call
server: string; // MCP server ID
tool: string; // tool name
arguments?: any; // arguments passed to the tool
result?: any; // tool return value
error?: string; // set if the tool call failed
}
```
## Return Values
The return type is `agent.Next | null`.
**`null`** — default. The LLM's `completion.content` is surfaced to the client as-is.
**`{ data }`** — replace the standard response with custom data. The `data` value is passed back to the caller as the `Next` field of the final response.
```typescript
return { data: { status: "success", id: 42 } };
```
**`{ delegate }`** — skip the current response and hand off to another agent. The delegated agent runs with the same `ctx` (same space, writer, stack trace). See [Delegation](#delegation-from-next) below.
```typescript
return {
delegate: {
agent_id: "yao.specialist", // required
messages: [...], // required — messages to send
options: { metadata: {} }, // optional — override connector, locale, etc.
},
};
```
**`{ data, metadata }`** — `metadata` is for debug / tracing only and does not affect output.
```typescript
return {
data: { status: "done" },
metadata: { duration_ms: 120, steps: 3 },
};
```
> **Decision logic** (from `processNextResponse` in `next.go`):
> 1. `null` → standard response (LLM completion surfaced)
> 2. `{ delegate }` → call target agent, return its response
> 3. `{ data }` → wrap `data` in response, skip completion surfacing
> 4. `{}` (empty, no delegate or data) → same as `null`
## Common Patterns
### Handle Errors
> **Note:** In the current pipeline, if the LLM or tool execution fails the agent returns early _before_ Next Hook runs, so `payload.error` is rarely set. This guard is still useful as a safety net for future changes or custom call paths.
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
ctx.Send({
type: "error",
props: { message: payload.error, code: "llm_error" },
});
return { data: { status: "error" } };
}
return null;
}
```
### Parse Structured Output
When the LLM returns JSON, parse it in Next Hook:
```typescript
import { agent, Process } from "@yao/runtime";
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
const raw = payload.completion?.content;
const content = typeof raw === "string" ? raw : "";
let result: any;
try {
const match = content.match(/```json\n([\s\S]*?)\n```/);
result = JSON.parse(match ? match[1] : content);
} catch {
ctx.Send("Failed to parse response. Please try again.");
return { data: { status: "parse_error" } };
}
// Save to database via Yao process
const id = Process("models.bookmark.Save", result);
ctx.Send(`Saved: ${result.title}`);
return { data: { status: "success", id } };
}
```
### Send Navigation Action
After saving data, navigate the user to the result page:
```typescript
ctx.Send({
type: "action",
props: {
name: "navigate",
payload: { route: `/agents/yao.my-assistant/detail?id=${id}` },
},
});
```
### Read State from Create Hook
Data stored in `ctx.memory.context` during Create Hook is available here:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
const startTime = ctx.memory.context.Get("start_time") as number;
const duration = Date.now() - startTime;
const mode = ctx.memory.context.Get("mode") as string;
ctx.Send(`Done in ${duration}ms (mode: ${mode})`);
return null;
}
```
## Delegation from Next
Route to another agent based on the LLM's response:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
const raw = payload.completion?.content;
const content = typeof raw === "string" ? raw : "";
if (content.includes("[[ESCALATE]]")) {
return {
delegate: {
agent_id: "yao.senior-agent",
messages: payload.messages,
},
};
}
return null;
}
```
## Multi-Step Loop
**This is the key mechanism for autonomous multi-step execution.** The `Next` Hook returns `delegate` pointing back to the same agent, triggering another Create → LLM → Next cycle.
> **Note:** Each `delegate` call is a new agent execution. Use `ctx.memory.chat` (not `ctx.memory.context`) to persist the step counter across rounds — `memory.context` is scoped to the current request context and is released when the context ends (default TTL: 30 min).
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
// Use chat-level memory — survives across delegate rounds
const step = (ctx.memory.chat.Get("step") as number) || 0;
if (step < 3 && needsMoreWork(payload)) {
ctx.memory.chat.Set("step", step + 1);
// Append LLM's response and a follow-up instruction
const next_messages = [
...payload.messages,
{ role: "assistant", content: payload.completion?.content },
{ role: "user", content: "Continue with the next step." },
];
return {
delegate: {
agent_id: ctx.assistant_id, // same agent — triggers another cycle
messages: next_messages,
},
};
}
ctx.memory.chat.Del("step"); // clean up when done
return null;
}
```
## Real Example: Mark's Next Hook
From `yaobots/assistants/yao/mark/src/index.ts` — reads sandbox output, saves to DB, navigates:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next {
if (payload.error) {
return { data: { status: "error", message: payload.error } };
}
const canvasFiles = readCanvasFiles(ctx);
const chatId = ctx.memory.context.Get("chat_id") || ctx.chat_id;
const mode = ctx.memory.context.Get("mode") || "create";
const fileId = saveJSAttachment(canvasFiles.js, `canvas_${Date.now()}.js`);
const { metadata } = canvasFiles;
const auth = getAuth(ctx);
let canvasId: string;
if (mode === "edit") {
canvasId = ctx.memory.context.Get("canvas_id");
updateCanvasRecord(ctx.memory.context.Get("record_id"), fileId, metadata, auth);
} else {
canvasId = generateCanvasId();
createCanvasRecord(canvasId, fileId, chatId, metadata, auth);
}
// Navigate to result
ctx.Send({
type: "action",
props: { name: "navigate", payload: { route: `/agents/yao.mark/index?canvas=${canvasId}` } },
});
const duration = Date.now() - (ctx.memory.context.Get("start_time") as number);
ctx.Send(`Bookmark "${metadata.title}" ready! (${duration}ms)`);
return { data: { status: "success", canvas_id: canvasId } };
}
```
## What's Next
You've seen what Hooks can do. Now see everything available inside them.
→ **[Context API](./context-api)**