---
sidebar_position: 5
title: Composition & fleets
---

# Composition — `dispatchesWorkflow` and `kind`

## What it is

An agent can run **another** catalog agent as a sub-graph. Deploying the parent
cascades: every declared member is installed as the parent's own child, bound to
it, and recorded on its row — so one Deploy gives you a whole team.

## Syntax — three forms

**1. The declarative sub-graph node.** The engine records the member for you:

```js
graph.addNode('build', {
  workflow: 'frontend-specialist',
  input:  (state) => ({ ticket: state.ticket }),
  output: (result) => ({ pr: result.prUrl }),
});
```

**2. A single member on a custom node:**

```js
export const poPlanNode = {
  name: 'plan',
  dispatchesWorkflow: 'product-owner',
  execute: async (state, ctx) => { /* … */ },
};
```

**3. A roster — one node fanning out over many members:**

```js
export const dispatchNode = {
  name: 'dispatch',
  dispatchesWorkflow: ['ticket-triage', 'developer', 'frontend-specialist',
                       'gitlab-code-review', 'generate-test-cases', 'product-owner'],
  execute: async (state, ctx) => { /* dispatches whichever the manager chose */ },
};
```

## Properties

| Name | Where | Type | Required | Allowed values | Default | Update behavior |
|---|---|---|---|---|---|---|
| `dispatchesWorkflow` | node | string \| string[] | No | catalog slugs | absent | Re-applied — a new member is installed on the next deploy |
| `kind` | `spec` | string | No | `fleet`, `app` | none | `fleet` is card-only; `app` changes the deploy path |
| `composedOf` | — | — | **derived, never declared** | — | — | Re-applied |

`kind: 'fleet'` moves the card into the Fleet section of the catalog and lets it
say "installs a team of N" *before* you install it. It does **not** change how
the agent deploys.

`kind: 'app'` is different in kind: the card is a hosted application you open in
a browser, not an agent, and it deploys down a different path.

## What deploy does

1. Builds one ordered member list — sub-graph children first, then anything
   named by [`requires`](./requires.md).
2. Installs each member (recursively), in parallel.
3. Records the bindings on the parent, so a dispatch at run time resolves to the
   right instance.
4. Records the member names on the parent's row for the Agents list.

If any member fails, **the whole deploy fails** and the rows this deploy created
are rolled back. You never end up with half a fleet.

## Gotchas

**A member can be a string or an array — always handle both.** A roster node
declares many members on one node; reading only the string form installs one and
leaves every other dispatch failing at run time.

**An agent cannot dispatch an ancestor.** A cycle is refused at deploy.

**A dispatch node runs no model of its own** and is excluded from "apply this
model to every node".

**A parent collects its children's model keys.** If your child runs a different
vendor, the deploy asks for that key too — and marks it non-blocking, so a
branch you will not use never stops the install.

**A member that subscribes to events is suppressed while it is a member.**
Otherwise one pull request would fire both the fleet (which runs the member
in-process) and the standalone member — a double review. Deploy the member
on its own and it starts responding to events again.

## See also

- [`requires`](./requires.md) — the other kind of member.
- [Deploy-time configuration](./deploy-time-config.md) — event subscriptions.
