Skip to main content

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 for what a page is (and is not), the freshness contract, and the permission model these commands enforce.
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 and crewship 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


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>

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:
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>

--file is a Layer-1 page definition — human-authored YAML, apiVersion: crewship/v1, kind: Page:
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 below. Three optional keys shape the page rather than its permissions: 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 replaces the spec: omit any of these on a re-apply and they are deleted.

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

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:
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 for the grammar and page get 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:
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. Backed by POST /api/v1/pages.

crewship page update <slug>

Backed by PATCH /api/v1/pages/{slug}. 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.
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.
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 to undo one.

crewship page delete <slug>

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.
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.
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:
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.
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:
A status.v1 panel ({items[{name, state: ok|warning|critical, label}]}) pushes a list rather than a number:
A table.v1 panel takes keyed row objects matching its declared columns, not positional arrays — this is the canonical shape the schema validates against:

Limits that turn into responses, not silent drops

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.
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 takes, so the two compose:
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.
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.

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

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

crewship page grants <slug>

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

crewship page export <slug>

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 uses for the marketplace — a “page template” is not a separate object, it is an exported page spec.
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.
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.

crewship page import [bundle.yaml]

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>

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 exists to prevent. Up to the last 50 versions are kept.

crewship page versions <slug>

Lists the page’s structural history — who changed the layout, when, and what version number to hand page rollback. 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.
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.
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>

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 instead — one write path, provenance attached server-side, and no credential to leak.
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 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:
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. A token whose panel is deleted from the page spec dies with it, the same way the panel’s payload ring does.

Flags

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. What a producer script needs to branch on: 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.