> ## 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.

# Page Commands

> Create, permission, and feed data into Pages — panel dashboards a routine or script pushes to, never queries.

# crewship page

Manage Pages — workspace-scoped dashboards built from panels. A page holds no
datasource and no credentials; a panel renders the last payload its declared
producer pushed. See the [Pages guide](/guides/pages) for what a page is (and
is not), the freshness contract, and the permission model these commands
enforce.

```bash theme={null}
crewship page <subcommand> [flags]
```

Routes are workspace-unscoped (`/api/v1/pages/...`); the CLI supplies the
workspace from your current context the same way it does for
[`crewship saved-view`](/cli/saved-view) and [`crewship routine`](/cli/routine).

Every subcommand honours the global `--format table|json|yaml|ndjson|quiet`.
Machine output is the server's own document passed through unmodified rather
than re-encoded, so a field this build has never heard of is not lost on the
way out. `--format quiet` prints one key per line — the slug, the link id, the
webhook id, the action id, the version seq — so a listing can be piped into the
command that consumes it. A listing with nothing in it prints **nothing** under
`quiet`, rather than the sentence the human format would use: an empty pipe is
empty, and a line saying so would be read as a key. `page export` is the
exception in the other direction: it prints YAML unless you ask for
`--format json`.

## Subcommands

| Command                                                          | Purpose                                                              |
| ---------------------------------------------------------------- | -------------------------------------------------------------------- |
| `list`                                                           | List pages in the workspace                                          |
| `get <slug>`                                                     | Show one page's definition and panels                                |
| `create --file <file>`                                           | Create a page from a YAML definition                                 |
| `update <slug>`                                                  | Change an existing page's definition                                 |
| `delete <slug> --yes`                                            | Delete a page                                                        |
| `set <slug>/<panel> --data -`                                    | Push a panel's data — the single write path                          |
| `actions <slug>/<panel>`                                         | List the actions a panel declares, and the routine each one runs     |
| `action <slug>/<panel> <action-id>`                              | Dispatch a declared action                                           |
| `grant <slug> --user\|--crew\|--agent <subject> --level <level>` | Grant read, produce, or write on a page                              |
| `revoke <slug> --agent <agent-slug>`                             | Remove a grant                                                       |
| `grants <slug>`                                                  | List a page's grants                                                 |
| `webhook create <slug> --panel <id>`                             | Mint a URL that lets something outside the workspace write one panel |
| `webhook list <slug>`                                            | List a page's webhook tokens and say which still work                |
| `webhook revoke <slug> --id <id> --yes`                          | Withdraw one webhook token                                           |
| `export <slug>`                                                  | Export a page's spec as a portable bundle                            |
| `import <bundle> --slug <slug> --bind <crew>=<crew>`             | Import a bundle, binding its declared references                     |
| `versions <slug>`                                                | List the page's structural history                                   |
| `rollback <slug> --to <seq>`                                     | Roll back to a previous version                                      |
| `publish <slug>`                                                 | Publish a page behind an expiring public link                        |
| `links <slug>`                                                   | List a page's public links and which still work                      |
| `unpublish <slug> --id <id>`                                     | Withdraw one public link                                             |

***

## `crewship page list`

```bash theme={null}
crewship page list
```

Backed by `GET /api/v1/pages`. Returns the pages you may see — a page you
have no `read` grant for and no crew membership on any of its panels for is
not in the list.

`list` takes no flags of its own. Columns are `SLUG  NAME  PANELS  STATE
OWNER  LAST DATA`, where `PANELS` counts every panel on the page including the
ones sealed to you. `--format quiet` prints one slug per line, which is the
form the next command in a pipe wants.

***

## `crewship page get <slug>`

```bash theme={null}
crewship page get fleet-201
```

Backed by `GET /api/v1/pages/{slug}`. Prints the page definition and every
panel you may see. A panel whose owning crew you are not in, and for which you
hold no grant, arrives as a **sealed placeholder** — its id, its width, its tab
and its owning crew's name, and nothing else — so the page has the same shape
for every reader. In the human output that is one line:

```
[sluzby]  sealed — owned by Lookout, and not visible to you
```

In `--format json` the panel carries `"sealed": true`. Key on that field being
present rather than on other fields being absent: a serialisation bug and a
permission decision are opposite failures, and only one of them is safe.

***

## `crewship page create --file <file>`

```bash theme={null}
crewship page create --file fleet-201.page.yaml
```

`--file` is a Layer-1 page definition — human-authored YAML, `apiVersion:
crewship/v1`, `kind: Page`:

```yaml theme={null}
apiVersion: crewship/v1
kind: Page
metadata:
  name: Flotila .201
  slug: fleet-201
spec:
  panels:
    - id: sluzby
      schema: status.v1
      title: Jede to?
      icon: container                # optional; default is the schema's own
      tab: Provoz                    # optional; no tab anywhere = no tab bar
      owner: crew/lookout            # permission anchor, not a label
      producer: script/watch-services.sh
      sla: 30s
      span: 8

    - id: queue-depth
      schema: metric.v1
      title: Hloubka fronty
      icon: queue
      tab: Provoz
      owner: crew/lookout
      producer: script/watch-queue-depth.sh
      sla: 60s
      span: 4

    - id: inventory
      schema: table.v1
      title: Sklad
      tab: Sklad
      owner: crew/lookout
      producer: webhook/sklad
      sla: 1h
      span: 12

    - id: rozbor
      schema: narrative.v1
      title: Co se dělo v noci
      tab: Provoz
      owner: crew/lookout
      producer: agent/riley       # pushed from inside a container, via the sidecar
      sla: 12h
      span: 12
```

Every panel this page needs is declared here, and every example further down
pushes to one of them — so the commands below can be pasted in order against a
page you actually created.

The CLI parses this file and sends the **parsed spec as JSON**, not the YAML
text — the server validates a structured spec, not an opaque string. `sla:
30s` is YAML sugar; on the wire it travels as `sla_seconds` (an integer).

Every panel needs `sla`; a panel without one does not validate — there is no
default that means "never mind" (§4). `owner` names the crew whose membership
gates who can see the panel; it is the permission anchor, not decoration.
`producer` names the routine, script, agent or webhook allowed to push data
into it — see [`page set`](#crewship-page-set) below.

Three optional keys shape the page rather than its permissions:

| Key       | What it does                                                                                                                                                                                                                                                                                                      |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `icon:`   | The panel's glyph, from a closed set of thirteen. A name outside it is refused locally, and the refusal lists the whole set.                                                                                                                                                                                      |
| `tab:`    | The tab this panel renders under. One word on the panel and no `tabs:` block: bar order is first appearance, a panel naming no tab lands on the first one, and a page where nothing names a tab has no bar. At most 8 tabs, 32 characters each. See [Pages → tabs](/guides/pages#tabs--several-screens-one-page). |
| `public:` | Opts this one panel into a published page. Default deny, per panel, and only a human may set it.                                                                                                                                                                                                                  |

`span:` is 1–12 and defaults to 12, the full width of the grid. A page holds at
most 24 panels and 256 KiB of spec.

Remember that [`page update --file`](#crewship-page-update-slug) **replaces**
the spec: omit any of these on a re-apply and they are deleted.

### `--owner crew/<slug>` — whose page it is

```bash theme={null}
crewship page create --file provoz.page.yaml --owner crew/ops
```

Without it, the page belongs to whoever ran the command. That is right for a
personal page and wrong for a team's board, because **page ownership is what
decides who may hand-write a `script`- or `webhook`-produced panel** — and
ownership of a crew-owned page counts every member of that crew. Hand the page
to `crew/ops` and anyone in ops can correct a panel by hand, without a grant and
without stopping the producer that normally writes it. (A workspace admin can do
that on any page, owned or not.)

Note the two different owners. A *panel's* `owner:` decides who may **see** it.
The *page's* owner — and a workspace admin — may hand-write a `script`- or
`webhook`-produced panel and edit the spec. A panel produced by a **routine** or
an **agent** is writable only by that producer, or by a subject holding an
explicit `produce` grant, no matter who owns the page. The two owners are set in
different places on purpose: the panel's is part of the document, the page's is
not.

`--owner` takes `crew/<slug>` and nothing else — not `ops`, not `user/<id>`.
Your own id is already the default, and the crew has to exist in this workspace
or the create is refused naming it. Ownership is a create-time decision:
`page update` re-applies a spec and never moves it, because a re-apply that
silently transferred a page would be a permission change nobody asked for. There
is no transfer command at all — ownership moves only when the owning user is
erased from the workspace, and then only to a crew.

`page get` and `page list` both show the owner, so what you set is visible
without reading the database.

A panel may also declare the sensor half — a `wake:` threshold that opens an
issue on a crew when the pushed payload crosses it, and `on_failure:` for when
it stops arriving at all:

```yaml theme={null}
    - id: sluzby
      schema: status.v1
      owner: crew/lookout
      producer: script/watch-services.sh
      sla: 60s
      on_failure:
        issue: crew/lookout          # SLA lapses → an issue, once per lapse
      wake:
        - when: any(state == "critical")
          for: 5m                    # must hold this long — one bad scrape wakes nobody
          agent: crew/devops         # who gets woken
          writes: queue-depth        # where they are expected to answer
```

`writes:` has to name a panel **on this page** — a gate pointing at a panel
that is not there is refused when the page is saved. It also does not grant
anything: the woken agent still needs produce authority on that panel, which on
the container door means the panel declares `producer: agent/<its slug>` or a
human granted it `produce`. A gate whose target declares `producer: routine/…`
is answered by running that routine, not by the woken agent writing into it.

The CLI sends both verbatim and the server parses them: `when` is checked
against the panel's own schema, and a predicate that panel could never satisfy
is refused here rather than accepted and silently never matched. See
[Pages → wake gates](/guides/pages) for the grammar and
[`page get`](#crewship-page-get-slug) plus `crewship automation list` for
seeing what a gate became.

A panel produced by a **routine** may also declare `refresh:` — `on:wake` or
`on:panels-changed`, and nothing else. It is the event that *runs* that panel's
producer, so the analysis is already on the page when a human arrives instead of
starting when they get there:

```yaml theme={null}
    - id: incident
      schema: narrative.v1
      owner: crew/devops
      producer: routine/incident-rozbor
      refresh: on:wake             # run when a gate anywhere on this page fires
      sla: 1h
```

The CLI refuses a value outside those two before it sends anything, and the
server refuses a `refresh:` whose producer is not a `routine/` (it cannot run a
script, call a webhook producer, or dispatch an agent), an `on:wake` on a page
that declares no gate, and an `on:wake` on a panel that declares its own gate —
which is a loop. Like a gate, it compiles to a row `crewship automation list`
shows. See [Pages → `refresh`](/guides/pages#refresh--the-panel-that-pulls-itself).

Backed by `POST /api/v1/pages`.

***

## `crewship page update <slug>`

```bash theme={null}
crewship page update fleet-201 --file fleet-201.page.yaml
```

Backed by `PATCH /api/v1/pages/{slug}`.

| Flag     | Required | Description                                                                 |
| -------- | :------: | --------------------------------------------------------------------------- |
| `--file` |     ✓    | The page document to replace the spec with (YAML or JSON; `-` reads stdin). |

This **replaces** the spec, it does not merge into it: a panel field left out
of the file is deleted, including `tab:`, `icon:`, `public:`, `actions:`,
`wake:`, `on_failure:` and `refresh:`. Panels are reconciled by id, so a panel
that survives the edit keeps its payload ring.

<Warning>
  **Neither read command emits a document you can feed straight back.** `page
      get --format json` returns the READ shape — `sla_seconds` as an integer,
  plus `state`, `data`, `provenance` and an `authored` flag — and `update   --file` refuses it (`field authored not found in type pages.Document`).
  `page export` returns a *bundle*, a different format again, and it
  deliberately drops `wake:`, `on_failure:`, `actions:`, `refresh:` and
  `public:`.

  So keep the document you authored. If you have lost it, rebuild it by hand
  from `page get --format json`: wrap it in `apiVersion` / `kind` / `metadata`
  / `spec.panels`, turn `sla_seconds: 300` back into `sla: 300s`, and drop
  everything the server attached. Check the panels that carry `public: true`
  survive the trip — that flag is the one whose loss is silent.
</Warning>

If the document declares a `metadata.slug` different from the slug you passed,
the CLI refuses before sending: a page's slug is its address. `--owner` is
create-only and `update` never sends it, so a re-apply never moves ownership.

Every save — through this command, the in-app editor, or an agent with a
`write` grant — becomes a new row in the page's version history. See
[`page rollback`](#crewship-page-rollback-slug) to undo one.

***

## `crewship page delete <slug>`

```bash theme={null}
crewship page delete fleet-201 --yes
```

| Flag          | Required | Description                                                                                                                                        |
| ------------- | :------: | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--yes`, `-y` |     ✓    | Confirms the delete. Consistent with every other destructive Crewship CLI command — a delete an agent cannot script non-interactively is unusable. |

Backed by `DELETE /api/v1/pages/{slug}`.

***

## `crewship page set`

The single write path for panel data. Everything that produces a number,
status, or table on a page — a cron job, a script on a host you control, a
routine step — ends here.

```bash theme={null}
echo '{"value": 42, "unit": "jobs"}' | crewship page set fleet-201/queue-depth --data -
```

| Flag      | Required | Description                                                                                                                                                                                                                                                                                       |
| --------- | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--data`  |          | The panel payload, JSON. `-` reads it from stdin — the form every producer script uses.                                                                                                                                                                                                           |
| `--state` |          | The producer's own verdict: `ok` (the default) or `failed`. A producer that ran and could not measure pushes `failed` with whatever it does know; the panel then shows a failure rather than ageing quietly into staleness. `fresh` and `stale` are the server's arithmetic and are refused here. |

Backed by `PUT /api/v1/pages/{slug}/panels/{id}/data`. `page set` authenticates
with your session or CLI token the way any other `crewship` command does, and a
producer script always calls the same command wherever it runs.

<Note>
  A producer running **inside a crew container** does not use this command, and
  could not: there is no `crewship` binary in the sandbox image. It pushes to
  its sidecar — `PUT http://localhost:9119/pages/{page}/{panel}`, body = the
  payload, `?state=failed` when it ran and could not measure — which attaches
  the identity from the agent's own token and forwards to
  `PUT /api/v1/internal/pages/{page}/data`. The credential stays in the sidecar
  and never enters the agent process:

  ```bash theme={null}
  curl -s -X PUT http://localhost:9119/pages/fleet-201/rozbor \
    -H "Content-Type: application/json" \
    -d '{"blocks":[{"kind":"paragraph","text":"Dva restarty ve 03:12, oba se zvedly."}],"verdict":"Noc bez zásahu."}' \
    -K /dev/fd/3 3<<AUTH
  header = "Authorization: Bearer $CREWSHIP_AGENT_TOKEN"
  AUTH
  ```

  The `fd-3` form is deliberate: a bearer token passed as `-H` is visible in
  the container's process table.

  Note the panel: `rozbor`, not `sluzby`. `rozbor` declares `producer:
      agent/riley`, and the curl above runs inside riley's container. `sluzby`
  declares `producer: script/watch-services.sh`, and this door would refuse it
  — which is the rule stated immediately below, so an example that pushed there
  would be teaching the opposite of the paragraph it sits in.

  **Declare such a panel `producer: agent/<slug>`, not `producer: script/…`.**
  A `script` producer means "a human's CLI token pushes this", and the
  unattended path admits only the declared `routine/` or `agent/`. A container
  writing a `script`-declared panel is refused with 403 unless a human issued
  it a `produce` grant. See
  [Pushing from inside a container](/guides/pages#from-inside-a-crew-container).
</Note>

The server, not the script, attaches provenance (`producer`, `run_id`,
`produced_at`) and the freshness clock. A payload the caller cannot prove they
own — the caller is not the panel's declared `producer` — is refused with
**403**, and the refusal itself writes a journal entry and notifies the page
owner (§7.1b rule 3): treat a 403 here as a signal to investigate, not noise
to retry past.

### A working producer script

The panel above is `metric.v1`
(`{value, unit?, delta?, target?, sparkline?[]}`). A cron job or a step inside
a routine can push it with nothing more than `jq` and the CLI:

```bash theme={null}
#!/usr/bin/env bash
# watch-queue-depth.sh — runs identically from a workstation or a cron job.
# NOT from inside a crew container: there is no crewship CLI in that image,
# and the sidecar door pushes as an agent (see the note above).
set -euo pipefail

depth=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" llen jobs:pending)

jq -n --argjson value "$depth" '{value: $value, unit: "jobs"}' \
  | crewship page set fleet-201/queue-depth --data -
```

A `status.v1` panel (`{items[{name, state: ok|warning|critical, label}]}`)
pushes a list rather than a number:

```bash theme={null}
jq -n '{items: [
  {name: "api",     state: "ok",       label: "200 OK"},
  {name: "worker",  state: "critical", label: "3 restarts in 5m"}
]}' | crewship page set fleet-201/sluzby --data -
```

A `table.v1` panel takes **keyed row objects** matching its declared
`columns`, not positional arrays — this is the canonical shape the schema
validates against:

```bash theme={null}
jq -n '{
  columns: [{key: "sku", label: "SKU"}, {key: "qty", label: "Qty"}],
  rows: [
    {sku: "A-100", qty: 12},
    {sku: "B-220", qty: 3}
  ]
}' | crewship page set fleet-201/inventory --data -
```

### Limits that turn into responses, not silent drops

| Limit                                                     | Response                               |
| --------------------------------------------------------- | -------------------------------------- |
| Payload over 64 KiB                                       | `422`, rejected at the handler         |
| Two payloads stored for one panel less than **2 s** apart | `429` with `Retry-After`               |
| More than 12 pushes/min sustained (burst 30) to one panel | `429` with `Retry-After`               |
| More than 600 pushes/min across the workspace             | `429` with `Retry-After`               |
| Caller is not the panel's declared producer               | `403`, journalled, page owner notified |

The 2 s floor is the one a `--loop 1` producer meets first, and it is not a
token bucket: it is enforced inside the write itself, so it holds across
replicas and across all four doors. The CLI prints the scope and the wait —
`this panel is being pushed faster than its rate limit allows — retry in 2s` —
and exits 6, so a loop can sleep exactly that long rather than guess.

***

## `crewship page actions`

List the actions a panel declares.

```bash theme={null}
crewship page actions fleet-201/sluzby
```

```
ID           KIND    LABEL        RUNS                     CONFIRM  INPUTS
restart-api  call    Restart API  routine/restart-api      yes      reason*
open-issue   link    Open ENG-15  issue/ENG-15             —        —
collapse     toggle  Collapse     panels inventory         —        —
```

Backed by `GET /api/v1/pages/{slug}/panels/{id}/actions`. The list comes from
the page's **stored spec** — the same list the server resolves a click against
— so what you see is exactly what can be dispatched. A panel you may not see
answers `404`, the same answer an unknown panel gives, so an action list is
never an existence oracle for somebody else's crew.

Only `call` actions reach the server. A `link` navigates to an internal entity
(`issue`, `run`, `page`, `agent`) and the renderer builds the address — there is
no URL field anywhere in the schema and there will not be one. A `toggle` is
client-side panel state. A `custom` action resolves to a handler compiled into
the web client.

`--format quiet` prints one action id per line — column 0 is the argument
[`page action`](#crewship-page-action) takes, so the two compose:

```bash theme={null}
crewship page actions fleet-201/sluzby -f quiet |
  while read -r id; do crewship page action fleet-201/sluzby "$id"; done
```

A panel that declares no actions prints nothing at all under `quiet` and exits
`0`. Do not read the empty output as a failure — a panel with no actions and a
panel you may not see are told apart by the exit code, not by the text.

***

## `crewship page action`

Dispatch one of a panel's declared actions.

```bash theme={null}
crewship page action fleet-201/sluzby restart-api --input reason="deploy wedged"
```

```
Queued: restart-api on fleet-201/sluzby runs routine/restart-api.
  pending: pnd_01k2r7wn8q0000000000001
  fires:   2026-08-12T09:14:22Z
  This returned when the run was accepted, not when it finished.
```

| Flag                | Required | Description                                                                                                       |
| ------------------- | :------: | ----------------------------------------------------------------------------------------------------------------- |
| `--input k=v`       |          | An input the action declared. **Repeatable**; a value may contain a comma, so this is not a comma-separated list. |
| `--idempotency-key` |          | Pin the dedupe key. Defaults to a fresh UUID per invocation.                                                      |

Backed by `POST /api/v1/pages/{slug}/panels/{id}/actions/{actionId}`.

### You cannot name a routine here, and that is the design

There is no `--routine` flag, and the request body carries **only** the
collected inputs. The server resolves the action id against the page's stored
spec and dispatches the routine named *there*. A compromised client, an injected
narrative panel, or an agent cannot choose what runs, because the wire format
has no field for it. The allow-list is not a check the server remembers to
perform — it is the only path that exists.

Inputs are validated server-side against the action's own declaration. An input
the action did not declare is **refused**, not passed through; a required input
that is missing is refused before anything runs; a fixed `param` the page author
set is not yours to override.

### It returns when the run is queued, not when it finishes

The answer is `202` with a pending id. Nothing waits for the run — a page button
on a ten-minute routine must not hold a connection. Watch the run itself on the
page, in the activity feed, or with `crewship routine runs`.

### Retrying is safe, replaying with different inputs is not

Every dispatch carries an `Idempotency-Key`, generated locally, one per
invocation. Pass `--idempotency-key` to pin it so a retry from a shell loop or a
CI step resolves to the original dispatch instead of starting a second run.

Reusing a key with **different** inputs is refused with `409` rather than
silently deduped onto the first run — a replayed key that quietly returned
somebody else's result would tell you a click succeeded that never happened.

| Response          | Meaning                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| `202` `SCHEDULED` | Queued. `pending_id` is the receipt.                                                                          |
| `202` `DEDUPED`   | Same key, same inputs — the original dispatch, not a second run.                                              |
| `400`             | The inputs do not satisfy the action's declaration, or the action is not a `call`.                            |
| `403`             | You can see the panel but may not run routines (MANAGER+, the same floor the routine's own endpoint applies). |
| `404`             | No such action on this panel — or no such panel, for you. The two are deliberately indistinguishable.         |
| `409`             | The key was replayed with different inputs, or the routine is missing / awaiting approval / disabled.         |
| `429`             | This action is already running. Carries `Retry-After`.                                                        |

### Who may dispatch

Two halves, and both must hold:

1. You can **see the panel** — membership of its owning crew, or a workspace
   admin role. A caller who would get a sealed placeholder gets `404`, not
   `403`: the action does not exist for them, and a `403` would confirm it
   exists for someone else.
2. You hold what the **routine** requires — MANAGER+, and the routine must be
   `active`. A page button is never a cheaper way to run a routine than the
   routine's own surface.

Every dispatch is journalled as `page.action.dispatched`, carrying who clicked,
which action, and which routine the server resolved it to.

### Declaring an action

Actions are declared in the page document, by a human editing the page. An agent
can write a panel's data; it can never author the button underneath it.

```yaml theme={null}
panels:
  - id: sluzby
    schema: status.v1
    owner: crew/lookout
    producer: script/watch-services.sh
    sla: 30s
    actions:
      - id: restart-api
        kind: call
        label: Restart API
        style: danger              # default | primary | danger
        routine: restart-api       # resolved at save time and at click time
        params:                    # fixed, author-controlled
          cluster: prod
        confirm:                   # drawn by host chrome, never by panel content
          title: Restart the API?
          body: In-flight requests are dropped.
        inputs:                    # collected from the user, validated server-side
          - name: reason
            type: text             # text | textarea | number | boolean | select
            required: true
      - id: open-incident
        kind: link
        label: Open the incident
        ref: { kind: issue, id: ENG-15 }
      - id: collapse
        kind: toggle
        label: Collapse
        target: [inventory]        # every target must be a panel on this page
```

Refused at save, every time:

* an action `kind` outside `call | link | toggle | custom`, or none at all;
* a `call` that names no routine, or names one that does not exist here;
* a `link` carrying anything URL-shaped instead of an entity id;
* a `toggle` targeting a panel that is not on the page;
* two actions on one page sharing an id;
* an input named after a fixed param, or an input collecting a secret;
* actions on a `narrative.v1` panel — a panel that renders agent-written prose
  and can also trigger an action is refused in this release.

***

## `crewship page grant <slug>`

Grants layer on top of a panel's crew-based visibility — they widen who may
reach the page, never who may reach a crew's data (§7.1 rule 3, §7.1b).

```bash theme={null}
crewship page grant fleet-201 --agent devops-bot --level produce --panels sluzby,queue-depth
crewship page grant fleet-201 --crew  finance     --level read
crewship page grant fleet-201 --user  ops@example.com --level write
```

| Flag                             | Description                                                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--user <email>`                 | Grant to a human user                                                                                                                                    |
| `--crew <slug>`                  | Grant to a crew                                                                                                                                          |
| `--agent <slug>`                 | Grant to an agent                                                                                                                                        |
| `--level <read\|produce\|write>` | The verb being granted — see [the permission model](/guides/pages#the-three-verbs)                                                                       |
| `--panels <id,id,...>`           | Restricts a `produce` grant to these panel ids. Omitted = every panel the subject is otherwise eligible to write. Only meaningful for `--level produce`. |

<Note>
  Only a human can run this command successfully. An agent holding `write` on
  the page can rebuild its layout freely but cannot issue a grant — not even
  to another agent in its own crew — by any route, including this one (§7.1b
  rule 1).
</Note>

Every grant change is journalled (`page.grant_added`), so who granted what to
whom is always answerable later.

Backed by the page's grants endpoint (`GET`/`PUT`/`DELETE …/grants`).

***

## `crewship page revoke <slug>`

```bash theme={null}
crewship page revoke fleet-201 --agent devops-bot
```

Removes a grant, journalled as `page.grant_removed`.

`revoke` takes the same three subject flags as `grant` — `--user`, `--crew`,
`--agent` — plus an optional `--level`. Omit the level and every level that
subject holds is withdrawn at once.

<Note>
  A grant can also stop working without being revoked. It is live only while
  the human who issued it still has the reach they granted from: leave the
  workspace, lose ADMIN, or be removed from the page's owning crew, and every
  grant you issued narrows with you. The row stays — `page grants` shows it as
  inert, with the reason — so you can see what would come back if that person's
  access were restored. Revoking is how you remove a grant; this is how one
  stops being borrowed authority nobody is accountable for any more.
</Note>

***

## `crewship page grants <slug>`

```bash theme={null}
crewship page grants fleet-201
```

Lists every grant on the page: subject type, subject, level, and (for
`produce`) which panels it covers.

***

## `crewship page export <slug>`

```bash theme={null}
crewship page export fleet-201 > fleet-201.page.yaml
```

Produces a portable bundle: workspace-specific ids are stripped, and every
external reference the page needs (crews, producers) is declared so an
importer can see what it must bind before installing. This is the same
export/import mechanism [`crewship routine export`](/cli/routine#crewship-routine-export-slug)
uses for the marketplace — a "page template" is not a separate object, it is
an exported page spec.

<Note>
  Export carries the page **spec** only, not panel data. Data is state that
  belongs to the install it came from; moving it between workspaces would
  mean shipping numbers with no producer behind them.
</Note>

<Warning>
  The bundle carries a panel's structure — id, schema, owner, producer, SLA
  and span. It does **not** yet carry `wake:` gates or `on_failure:`, so an
  imported page arrives unmonitored and the gates have to be added again in
  this workspace. That is deliberate rather than silent: a gate names a crew,
  and a crew reference has to be bindable (`--bind`) before it can be carried,
  or an import would install a gate pointing at a crew that does not exist
  here and quietly wake nobody. Until then the bundle omits what it cannot
  bind.
</Warning>

***

## `crewship page import [bundle.yaml]`

```bash theme={null}
crewship page import weekly-close.page.yaml --slug uzaverka --bind crew/ucetni=crew/finance
```

| Flag                  | Required | Description                                                                                                          |
| --------------------- | :------: | -------------------------------------------------------------------------------------------------------------------- |
| `--slug`              |          | Slug to give the imported page in this workspace                                                                     |
| `--bind <ref>=<crew>` |          | Binds a declared external reference in the bundle to a local crew. Repeat for each reference the bundle needs bound. |

Import either binds every declared reference and creates the page, or refuses
and names which reference it could not resolve — it does not create a page
full of dead panels pointing at a producer that does not exist locally.

`--bind` is repeatable, once per reference, and is deliberately not
comma-separated: both halves are slugs, and a slug may plausibly contain a
comma where a flag may simply be repeated. Binding one reference twice is
refused rather than resolved last-wins.

***

## `crewship page rollback <slug>`

```bash theme={null}
crewship page rollback fleet-201 --to 3
```

| Flag          | Required | Description                                                                                         |
| ------------- | :------: | --------------------------------------------------------------------------------------------------- |
| `--to`        |     ✓    | Target version number to roll back to                                                               |
| `--yes`, `-y` |          | Skip the confirmation prompt. Required in a script — without it the command reads stdin and aborts. |

Rolls back the page **structure** — which panels exist, their layout, their
owners and producers. It never resurrects old panel data: a panel a rollback
brings back renders dimmed, in a "waiting for first data" state, even if rows
for it survive in the payload ring. The ring is cleared for the same reason for
a panel whose schema, producer or owning crew the rollback changed — the footer
would otherwise credit a producer that did not produce the number, or show a
payload to a crew the restored spec never admitted. Showing an old payload as current after a
rollback is exactly the dishonesty the [freshness
contract](/guides/pages#the-freshness-contract) exists to prevent. Up to the
last 50 versions are kept.

***

## `crewship page versions <slug>`

```bash theme={null}
crewship page versions fleet-201
```

Lists the page's structural history — who changed the layout, when, and what
version number to hand [`page rollback`](#crewship-page-rollback-slug). Up to
the last 50 versions are kept. Panel *data* is not versioned here; a version is
a record of what the page was shaped like, not of what it showed.

***

## `crewship page publish <slug>`

Publish a page to someone outside the workspace, behind an expiring link.

```bash theme={null}
crewship page publish uzaverka --expires-in-days 7
crewship page publish uzaverka --show-provenance
printf '%s' "$PASSWORD" | crewship page publish uzaverka --password-stdin
```

| Flag                | Required | Description                                                                                                                                                                                                                 |
| ------------------- | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--expires-in-days` |          | Days until the link stops working. Default 30, maximum 365. Omitting it sends no field at all, so the server's default applies and the two cannot drift.                                                                    |
| `--password-stdin`  |          | Read a password for this link from stdin, or prompt with echo off on a terminal. There is deliberately no `--password <secret>`: argv is visible in `ps` to every user on the box, and in the shell history afterwards.     |
| `--show-provenance` |          | Include producer names and run ids on the public page. Stripped by default — producer and routine names are internal vocabulary, and a public page that leaks them describes your infrastructure to whoever holds the link. |

The URL is printed **once**. The token is stored as a SHA-256 digest, so
nothing can show it again — not this command, not `page links`, not the
database. Lose it and you mint a new one.

Publishing exposes only the panels explicitly marked public, never the whole
page. See [Public pages](/guides/pages#public-pages).

***

## `crewship page links <slug>`

```bash theme={null}
crewship page links uzaverka
```

Lists a page's public links and says which of them still work — expired,
password-protected, and what each one exposes. It cannot show you a link
again; see `page publish` above for why.

***

## `crewship page unpublish <slug>`

```bash theme={null}
crewship page unpublish uzaverka --id cmsq1f8x0000 --yes     # id from `page links`
```

| Flag          | Required | Description                                              |
| ------------- | :------: | -------------------------------------------------------- |
| `--id`        |     ✓    | The link to withdraw, from `crewship page links <slug>`. |
| `--yes`, `-y` |          | Skip the interactive confirmation prompt.                |

Withdraws one link without touching the others. Several links per page is the
intended shape — revoking the one you sent a supplier does not break the one on
the wall display.

***

## `crewship page webhook`

Let a producer that **cannot run this binary** write one panel: a cron on
someone else's box, a Zapier step, a PLC gateway, a GitHub Action. Anything
that can run the CLI should use [`page set`](#crewship-page-set)
instead — one write path, provenance attached server-side, and no credential
to leak.

```bash theme={null}
crewship page webhook create uzaverka --panel cron --name "PLC hall 2"
crewship page webhook list uzaverka
crewship page webhook revoke uzaverka --id pgwh_... --yes
```

`create` prints the URL **once** and nothing can show it again: the token is
stored as a SHA-256 digest, the same at-rest shape [pipeline
webhooks](/cli/webhook) use. Copy it into the sender's secret store, or mint a
second one — several tokens per panel is the intended shape, so revoking the
PLC's does not break the GitHub Action's.

The sender POSTs the panel's payload as the body, with no envelope around it:

```bash theme={null}
curl -X POST https://crewship.example.com/api/v1/page-webhooks/pgw_... \
     -H 'Content-Type: application/json' \
     -d '{"items":[{"name":"linka-2","state":"ok","label":"412 ks/h"}]}'
```

A producer that ran and failed says so on the query string —
`?state=failed` — with whatever payload it has. `fresh` and `stale` are the
server's arithmetic and are not a sender's to claim, and neither is
`produced_at`: provenance is attached server-side, so a body carrying a
timestamp or a producer name is refused rather than believed.

### What the token can and cannot do

A webhook is a `produce` grant in a different coat, and it obeys every rule
that grant does.

|                                |                                                                                                                                                                                                                                                           |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Issued only by a human**     | An agent may build the page and may not mint a credential that writes it from outside.                                                                                                                                                                    |
| **Bound to one panel**         | Not one page — one panel. The panel is not on the wire, so there is no field through which a holder could name another one: a leaked token writes that panel and nothing else.                                                                            |
| **Never more than its issuer** | The token carries no authority of its own. Every request re-derives what the human who issued it may do *right now*, so it stops working the moment they leave the workspace, leave the crew, or lose the grant. Nobody has to remember the token exists. |
| **Rate limited per panel**     | The same push limits as every other write path, including the minimum interval enforced at the write. Minting a second token does not buy a second allowance.                                                                                             |
| **Revocable, and journalled**  | Revocation takes effect on the sender's next request. Every write is recorded with the token id as the actor, so an operator can tell which token to revoke.                                                                                              |

A token whose panel is deleted from the page spec dies with it, the same way
the panel's payload ring does.

### Flags

| Flag      | Command  | Purpose                                                                                         |
| --------- | -------- | ----------------------------------------------------------------------------------------------- |
| `--panel` | `create` | The panel this token writes. Required — there is no page-wide token.                            |
| `--name`  | `create` | A label for the listing ("PLC hall 2"), so tokens can be told apart when one has to be revoked. |
| `--id`    | `revoke` | The token to withdraw, from `webhook list`.                                                     |
| `--yes`   | `revoke` | Skip the confirmation prompt.                                                                   |

`webhook list` never shows a token value — the column holds a digest, so there
is nothing to show. A revoked token stays in the listing on purpose: "was it
used after we pulled it" is the question an incident asks, and a deleted row
cannot answer it.

***

## Exit codes

Standard [CLI exit codes](/cli/overview#exit-codes). What a producer script
needs to branch on:

| Code      | When                                                                                                     | What to do                                           |
| --------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `0`       | The push or dispatch was accepted                                                                        | For `page action`, accepted is not finished          |
| `2`       | Payload invalid, over the 64 KiB cap, or the spec/flags did not validate                                 | Fix it; retrying unchanged fails identically         |
| `3`       | No such page or panel — or none you may see                                                              | Do not retry                                         |
| `4`       | You are not the panel's declared producer and hold no `produce` grant, or you are not authenticated      | A 403 here is journalled and notifies the page owner |
| `5`       | An idempotency key was replayed with different inputs, or the routine is missing, unapproved or disabled | Do not retry with the same key                       |
| `6`       | Rate limited                                                                                             | Sleep for the interval the message names, then retry |
| `7` / `8` | Server error, or the request never got an answer                                                         | Safe to retry a `page set` — it is last-write-wins   |

Refused **locally**, before anything is sent (always exit 2): a page document
that does not validate, a `<page>/<panel>` address with no slash, an empty or
non-JSON `--data`, an `--owner` that is not `crew/<slug>`, a missing
`--file`/`--panel`/`--id`, a `--to` that is not positive, an `--input` that is
not `k=v`, a malformed or repeated `--bind`, a bundle whose `format` is not
`crewship-page-bundle/v1`, and a `--file` whose `metadata.slug` disagrees with
the slug you passed to `update`.

Everything else is the server's answer — including an unknown slug and an
unknown `--level`, both of which are checked there and not here.

## Related

* [Pages guide](/guides/pages) — what a page is, the freshness contract, and the full permission model.
* [`crewship routine`](/cli/routine) — the usual producer behind a panel.
* [`crewship automation`](/cli/automation) — what a panel's `wake` gate compiles to.
* [Internal IPC API](/api-reference/internal) — the sidecar trust boundary `page set` uses inside a container.
