> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Automations

> Run a routine whenever a journal event happens, without writing any glue.

# Automations

An **automation** is one rule: *when this kind of thing happens in this
workspace, run that routine.* It watches a single [journal](/guides/crew-journal)
event type, evaluates a predicate against each entry, and parks a
debounced run of a routine.

```bash theme={null}
crewship automation create \
  --name "triage on status change" \
  --event mission.status_change \
  --payload-equals action=status_changed \
  --routine post-status-triage \
  --input issue='{{ event.mission_id }}'
```

That is the whole feature. It exists because until now every trigger in
Crewship was hard-coded: a schedule could start a routine, a webhook could
start a routine, but "an issue changed status" could not, and there was no
place to say so.

## What an automation can and cannot do

An automation can **only enqueue** — or, for the one rule type nothing but
Pages writes today, open an issue. It never executes a routine inline and holds
no veto over anything.

That is a deliberate boundary, not a missing feature. Automations are matched
on the journal **write path** — the moment an event durably commits — so
anything they did synchronously would be latency added to every write in the
product. Matching is in-memory; the only thing that reaches the database is a
single row parked in the deferred-run queue, and even that happens on a
background flush rather than inline.

<Note>
  Automations are not [hooks](/guides/hooks). A hook is an **intercept**:
  crew-scoped, blocking, and able to refuse the thing it fired on. An
  automation is workspace-scoped, non-blocking, and reacts after the fact.
  They look similar on paper and guarantee opposite things, which is why
  they are separate.
</Note>

## Anatomy of a rule

| Field              | Meaning                                                                                                                                                                                               |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event_type`       | One journal entry type, e.g. `mission.status_change`. Exactly one — there is no wildcard.                                                                                                             |
| `matcher`          | Narrows which entries of that type match. Empty matches all of them.                                                                                                                                  |
| `action`           | `routine_slug` plus the `inputs` to run it with.                                                                                                                                                      |
| `action_kind`      | `routine` (the only kind `automation create` writes) or `issue`, which opens an issue on a crew instead of parking a run. Issue rules are written by [Pages' wake gates](/guides/pages), not by hand. |
| `debounce_seconds` | How long the parked run stays open for further events to fold into. Default `10`.                                                                                                                     |
| `max_per_hour`     | Cap on runs this rule may cause per hour. Default `60`.                                                                                                                                               |
| `enabled`          | A disabled rule never matches. It is not merely skipped later — it never enters the matcher at all.                                                                                                   |

### Choosing an event type

There is no wildcard by design: a rule that fires on "anything" is a support
ticket waiting to happen. Pick the one type you mean, and confirm it exists
before you save:

```bash theme={null}
crewship journal --type mission.status_change --lines 5
```

A typo is accepted by the API — the journal has 117 entry types and no
closed registry to check against — so it produces a rule that is saved,
listed, and never fires. Checking first costs one command.

### The matcher

Every predicate you set must be satisfied; the ones you leave out are "don't
care". Setting none means every entry of that type matches.

| Flag               | Matches when                                                     |
| ------------------ | ---------------------------------------------------------------- |
| `--crew`           | the entry came from one of these crews                           |
| `--agent`          | the entry came from one of these agents                          |
| `--mission`        | the entry is on one of these issues                              |
| `--severity`       | the entry's severity is one of `info`, `notice`, `warn`, `error` |
| `--payload-equals` | the named journal payload field equals this value                |

`--payload-equals` parses its value as JSON when it can, so `count=3` matches
the number `3` and `action=status_changed` matches the string
`"status_changed"`.

<Warning>
  `--payload-equals` has the same silent-failure mode as a typo'd `--event`,
  and a nastier one: a key **no emitter writes** is accepted, and the rule then
  matches nothing, forever, with no error anywhere. Read one real entry before
  writing the predicate:

  ```bash theme={null}
  crewship journal --type mission.status_change --lines 1 --format json
  ```

  and match on a key you can see in its `payload`.
</Warning>

### What `mission.status_change` actually carries

Every issue event goes through one emitter. It writes two keys on every
event, plus two more on a status transition:

| Key       | Value                                                                                                                                                                                                                                                                                                                                             |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action`  | What happened, from a closed set: `status_changed`, `priority_changed`, `review_approved`, `review_changes_requested`, `parent_changed`, `relation_added`, `description_changed`, `attachment_added`, `attachment_removed`, `code_link_added`, `code_link_removed`, `task_completed`, `task_failed`. This is the key to write predicates against. |
| `details` | Human-readable prose, e.g. `"BACKLOG → TODO"`. For display and for `{{ event.payload.details }}` — **not** for matching: it is a sentence and would break the day someone rewords it.                                                                                                                                                             |
| `from`    | The status before the change. Only on `action=status_changed`.                                                                                                                                                                                                                                                                                    |
| `to`      | The status after it. Only on `action=status_changed`.                                                                                                                                                                                                                                                                                             |

So **"fire when an issue moves to DONE" is one predicate**:

```bash theme={null}
crewship automation preview --event mission.status_change --payload-equals to=DONE
```

`from` and `to` appear only on a transition. A key that were always present
and sometimes empty would be a predicate that silently matched nothing, which
is the failure `automation preview` exists to surface.

To react to any status change rather than one target, match the action and let
the routine decide:

```bash theme={null}
crewship automation create \
  --name "triage on status change" \
  --event mission.status_change \
  --payload-equals action=status_changed \
  --routine post-status-triage \
  --input issue='{{ event.mission_id }}' \
  --input transition='{{ event.payload.details }}'
```

Some actions are precise enough to match on their own — `review_approved` is
one thing and one thing only:

```bash theme={null}
crewship automation create --name "on approval" \
  --event mission.status_change --payload-equals action=review_approved \
  --routine post-approval --input issue='{{ event.mission_id }}'
```

Note that `mission.status_change` is the CATCH-ALL issue entry type, not
"status changed specifically" — `created`, `assignee_changed`, `commented` and
`mentioned` have their own entry types, everything else lands here. That is why
the `action` predicate is usually needed and not redundant.

### Rules a Page wrote

A page panel's `wake:` gate compiles to a rule in this same table, named
`page <slug>/<panel> wake <n>`, with `action_kind: issue`. They appear in
`crewship automation list` like any other rule, which is the point — a gate you
cannot see is a gate you cannot debug.

They are **derived state**: the page spec owns them, every save of that page
rewrites them, and deleting the page deletes them. Disabling or editing one
here does not stick. Remove the gate from the page instead.

Their event type is `page.panel.updated`, which every accepted panel push
emits, and it carries:

| Key                | Value                                                                                                                                                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page` / `page_id` | The page's slug and id. Predicates use the id: a slug can be recycled.                                                                                                                                                                                         |
| `panel`            | The panel id that was pushed.                                                                                                                                                                                                                                  |
| `schema`           | The panel's schema, e.g. `status.v1`.                                                                                                                                                                                                                          |
| `producer`         | `<kind>/<ref>` — who was permitted to write it.                                                                                                                                                                                                                |
| `seq`              | The payload's sequence number within the panel's ring.                                                                                                                                                                                                         |
| `state`            | The producer's own verdict, `ok` or `failed`. Not the freshness state: `fresh` and `stale` are functions of the clock and are never stored.                                                                                                                    |
| `wake_<n>`         | Present, and `true`, ONLY when the panel's nth wake gate is armed by this push — its threshold is met and its `for` window has elapsed. This is what a gate's rule matches on, and it is why the threshold itself never has to be evaluated on the write path. |

### Inputs and the `event` namespace

Input values are rendered with the **same** template renderer routine steps
use, against the entry that triggered the rule:

| Reference                   | Resolves to                              |
| --------------------------- | ---------------------------------------- |
| `{{ event.mission_id }}`    | the issue the entry is on                |
| `{{ event.agent_id }}`      | the agent that produced it               |
| `{{ event.crew_id }}`       | the crew it belongs to                   |
| `{{ event.run_id }}`        | the run it came from, when there was one |
| `{{ event.payload.<key> }}` | one field of the entry's payload         |

An unresolvable reference renders empty, exactly as it does inside a routine.

## Burst control

Two independent controls, and they do different jobs.

**`debounce_seconds`** collapses a storm into one run. Two hundred status
changes on the same issue inside the debounce window produce a single run
carrying the most recent event's inputs, not two hundred runs. The parked run
also has a ceiling — `debounce_seconds × 10` from the first match — so a
stream of events that never stops still fires at the ceiling instead of being
pushed out forever, and the next match after it starts a fresh run.

Events collapse together only when they are about the **same subject**. The
subject is the most specific identity the entry carries: the issue, else the
run, else the agent, else the crew. So fifty `run.failed` entries for one run
are one triage run, but two different runs failing inside the window are two —
coalescing keeps the last event's inputs, and folding unrelated subjects into
one run would silently act on whichever arrived last. An entry that carries
none of those identities is genuinely workspace-scoped and collapses to the
rule itself.

**`max_per_hour`** caps how many runs the rule may cause per rolling hour. It
is charged per **run**, not per matched event — a burst that coalesces into
one run costs one unit. Over the cap, matches are dropped and exactly one
`automation.throttled` entry is written to the journal for that hour:

```bash theme={null}
crewship journal --type automation.throttled
```

One per hour, not one per drop. A rule that trips its cap ten thousand times
must not write ten thousand rows saying so.

<Warning>
  `max_per_hour` is a **burst brake, not a quota.** The counter is held in the
  running server's memory, so restarting the server clears it — a server that
  restarts every ten minutes has no effective hourly cap.

  Use it to stop a runaway rule. Do not use it where the number itself has to
  hold, such as billing or a contractual limit.
</Warning>

## Where rules show up in the UI

There is no automation management screen — the CLI above is the whole write
surface — but the two pages a rule affects both say so, read-only:

* **A routine's detail page** ([`/routines`](/guides/routines#the-detail-page))
  shows `N automations` beside its status pills whenever a rule targets it, and
  lists them under **Triggers → Automations**: rule name, the event type it
  watches, and whether it is armed. A disabled rule is shown greyed rather than
  hidden — a rule that is switched off is usually the answer to "why did
  nothing happen".
* **An issue's detail page** ([`/issues`](/guides/issue-detail)) grows an
  **Automations** card listing the rules an event from that issue could set
  off.

Both are absent when nothing applies. A routine no rule targets, and an issue
no rule watches, show nothing extra at all.

<Note>
  The issue card lists rules that **could** fire, not rules that will. Only
  `mission_ids` and `crew_ids` can be decided from an issue; `agent_ids`,
  `severities` and `payload_equals` describe an event that has not happened yet
  and narrow further at match time. A rule excluded by `mission_ids` or
  `crew_ids` is never listed — that exclusion is provable.
</Note>

## Watching a rule work

```bash theme={null}
crewship automation list
crewship journal --type automation.throttled     # is it being capped?
crewship pipeline runs --limit 10                # did the routine actually run?
```

Every run an automation causes carries the rule's id and how many events
folded into it in the run metadata, so a run can always explain why it exists.

## Permissions

Reading the list is available to any workspace member — "what fires here" is
the first thing anyone debugging an unexpected run needs. Creating, editing
and deleting are **ADMIN or OWNER**: a rule grants autonomous routine
execution across the workspace, on events its author may never produce
themselves.

## Deleting

Deletion is soft. The rule stops matching immediately and the row stays in the
database, because a run it caused is partly explained by the rule that caused
it — hard-deleting turns those runs into orphans.

## API

| Method   | Route                      | Role   |
| -------- | -------------------------- | ------ |
| `GET`    | `/api/v1/automations`      | member |
| `POST`   | `/api/v1/automations`      | ADMIN+ |
| `PATCH`  | `/api/v1/automations/{id}` | ADMIN+ |
| `DELETE` | `/api/v1/automations/{id}` | ADMIN+ |

`PATCH` is sparse: only the fields present in the body are written, so
toggling `enabled` cannot clobber a matcher somebody edited a moment ago.

Full flag reference: [`crewship automation`](/cli/automation).

## Limits

* One event type per rule. Compose several rules rather than asking for a
  wildcard.
* The action kind is `routine` in this release. The stored shape leaves room
  for others; anything else is rejected at write time rather than silently
  accepted and never run.
* A rule whose routine does not exist in the workspace is refused at
  create/update time, and one whose routine is deleted afterwards stops
  firing until the routine comes back.
