---
sidebar_position: 3
title: State & schema
---

# State & schema

Every agent run carries one **state** object that flows from node to node. State is the only handoff mechanism — there's no shared globals, no hidden context.

## Shape

```js
state = {
  input: { /* whatever the trigger passed */ },
  plan: { tasks: ['t1', 't2'] },        // ← from node 'plan'
  implement: { diff: '...' },            // ← from node 'implement'
  verify: { status: 'ok' },              // ← from node 'verify'
}
```

When a node completes, its validated output lands at `state[nodeName]`.

## Schema-validated handoff

Every node's `outputSchema` is a Zod schema. It runs *before* downstream nodes see the output:

```js
import { z } from '@zibby/core';

const Plan = z.object({
  tasks: z.array(z.string()).min(1),
  priority: z.enum(['low', 'normal', 'high']),
});

graph.addNode('plan', {
  prompt: 'Triage this ticket.',
  outputSchema: Plan,
  agent: 'claude',
});
```

If the agent returns malformed output, the node fails. Combined with `retries: N`, you get cheap automatic recovery from one-off LLM hallucinations.

Downstream nodes can reference plan output directly:

```js
graph.addNode('implement', {
  prompt: ({ state }) => `Implement these tasks:\n${state.plan.tasks.map(t => `- ${t}`).join('\n')}`,
  outputSchema: Implementation,
  agent: 'cursor',
});
```

## Function vs. template prompts

Two prompt forms:

```js
// String template — variables interpolated from state.
graph.addNode('plan', { prompt: 'Plan: {{input.goal}}', ... });

// Function — full programmatic control.
graph.addNode('plan', {
  prompt: ({ input, state }) => {
    const ctx = state.context?.summary ?? '';
    return `Plan: ${input.goal}\n\nContext:\n${ctx}`;
  },
  ...
});
```

Functions get `{ input, state, getAll, get }` so they can introspect state without throwing on missing keys.

## Skill hints

If a node opts into [skills](./skills), the framework appends prompt fragments that tell the agent how to use those tools. You don't write that boilerplate — register the skill once, list it on the node:

```js
graph.addNode('search', {
  prompt: 'Find info about {{input.query}}',
  outputSchema: Results,
  agent: 'cursor',
  skills: ['browser'],   // appends Browser MCP usage instructions to the prompt
});
```

## Rollback

State is history-tracked. To revert the last N writes (typical use case: a node validation passes Zod but the *content* is wrong, and you want to retry with extra instructions):

```js
const recovered = state.rollback(2);
```

`rollback` returns a fresh state with the last 2 writes dropped. Use it inside `onComplete` callbacks or custom retry logic.

## Reading state in templates

Inside the `prompt` string, `{{x}}` resolves first against `state.x`, then against `input.x`. Dotted paths work:

```
{{state.plan.tasks.length}}
{{input.ticket}}
```

Function prompts give you the same access without the templating layer.
