---
sidebar_position: 10
title: Node declarations
---

# Node declarations

Some declarations live on a **graph node**, not in the `spec` block,
because they are about what that node needs.

| Field | Declares |
|---|---|
| `skills` | the tools this node gets, and the integrations the agent requires |
| `optionalSkills` | integrations that improve the node but never block a deploy |
| `agent` | a vendor this node is locked to |
| `stores` | durable storage — see [`stores`](./stores.md) |
| `dispatchesWorkflow` | a member agent — see [Composition](./composition.md) |

---

## `skills` and `optionalSkills`

### What it is

A node's single "what this node needs" list. It drives four things at once:

1. the MCP tools the model gets **at that node**;
2. the skill's prompt fragment, appended to the node's prompt;
3. the required/optional integrations shown on the card and enforced at deploy;
4. for a no-connection skill, the on/off switch in the agent's Settings.

### Syntax

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

graph.addNode('review', {
  prompt: …,
  outputSchema: Review,
  skills: [SKILLS.GITHUB, SKILLS.ARTIFACT],
  optionalSkills: [SKILLS.SENTRY],
});
```

### Required vs optional

| Declared in | Surfaces as | Blocks Deploy? |
|---|---|---|
| `skills`, and the skill needs a connection | **required** | **Yes** — the Deploy button is blocked until it is connected |
| `skills`, and the skill is marked optional | optional | No |
| `optionalSkills` | optional | **Never** |
| the same skill in **both** arrays on one node | optional **for that node only** | No |

That last row is the useful one. A skill can be required by one node and merely
nice-to-have on another; listing it in a node's `optionalSkills` demotes it for
**that node** and cannot cancel a different node's requirement.

The card shows required and optional in separate groups — "Connect" versus
"Connect (optional)" — and only the required group can stop an install.

### One-of groups

Some entries in the array are not skills at all: they are markers that resolve
to *any one of* a set of providers.

| Marker | Satisfied by |
|---|---|
| `board_tracker` | Jira **or** Vikunja |
| `doc_source` | Google Docs **or** Notion **or** Lark Docs |
| `chat_notify` | Slack **or** Lark |

Connect any member and the requirement is met. Entries the platform does not
recognise are skipped with a warning, so an unknown id costs you nothing.

### Toggleable skills

A few skills need no connection at all — codebase memory, code scanning,
artifacts. They render as a simple on/off switch, default on, and the operator
can turn them off per agent.

:::danger Bind a toggleable skill on `skills`, not `optionalSkills`
The switch will appear either way — but **the tools will not**. Skills reach the
model only through a node's `skills`; `optionalSkills` is an integration-surface
declaration that the engine never reads. Put a toggleable skill in
`optionalSkills` and you ship a switch that controls nothing.
:::

### Gotchas

**`optionalSkills` is not part of the serialised graph.** It is read from your
source when the catalog is built, so it is visible on the card — but anything
that re-derives from a stored graph will not see it. Treat it as a card-level
declaration, never as a runtime one.

**A store skill is a precondition for [`stores`](./stores.md).** Without it, a
`stores` block on that node provisions nothing, silently.

---

## `agent` — pinning a node to a vendor

### What it is

Locks one node to one coding-agent vendor, regardless of what the operator picks
elsewhere.

### Syntax

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

### Properties

| Name | Type | Required | Allowed values | Default | Update behavior |
|---|---|---|---|---|---|
| `agent` | string | No | `claude`, `codex`, `gemini` | unpinned — the operator's choice applies | Frozen into the published graph; the credential slots are re-applied |

### What it does at deploy

The distinct set of pins across your graph becomes the agent's vendor
requirement:

| Pins found | Deploy asks for |
|---|---|
| none | any vendor — the picker offers all of them |
| one | that vendor only |
| several | one credential per vendor |

Children you dispatch contribute their pins too, so a parent on Claude that
dispatches a Codex child collects both keys.

### Gotchas

**A pinned node is invisible to "apply this model to every node".** It is
*locked* — that is the point — so it never receives an operator's model choice.
If every model-running node is locked, there is nothing left for the operator to
pick.

**There is no node-level `model:` declaration.** A model comes from one of three
places: the operator's per-node pick, a literal in your node's own code, or —
best — [`models[].fixedModelByVendor`](./models.md) joined to the node by
`role`. Only the third is visible on the card and the canvas. Prefer it.

## See also

- [`models`](./models.md), [`stores`](./stores.md),
  [Composition](./composition.md)
- [Skills](../concepts/skills.md)
