Substrate API
The substrate API is a per-user surface: every route operates on the substrate that belongs to the calling user, identified by the Bearer token. There is no {app_id} path segment.
Authentication
Section titled “Authentication”All routes accept either:
- A substrate-scoped API key:
Authorization: Bearer bb_sub_…(generate withbutterbase keys generate --substrate). - A platform JWT (dashboard / Cognito session).
Non-substrate-scoped API keys (bb_sk_…) are not accepted by these routes — they return 403.
Errors follow the standard envelope:
{ "error": { "code": "AUTH_INVALID_TOKEN", "message": "…", "remediation": "…" } }Settings
Section titled “Settings”| Method | Path | Purpose |
|---|---|---|
| GET | /v1/me/substrate/settings | Get yolo mode and other per-user toggles |
| PUT | /v1/me/substrate/settings/yolo | Toggle yolo mode |
PUT /v1/me/substrate/settings/yolo{ "yolo_mode": true }Actions
Section titled “Actions”| Method | Path | Purpose |
|---|---|---|
| POST | /v1/me/substrate/actions/propose | Propose a new action |
| GET | /v1/me/substrate/actions | List actions in the ledger |
| GET | /v1/me/substrate/actions/{action_id} | Fetch one action |
| POST | /v1/me/substrate/actions/{action_id}/approve | Approve a pending action |
| POST | /v1/me/substrate/actions/{action_id}/reject | Reject a pending action |
Propose
Section titled “Propose”POST /v1/me/substrate/actions/propose{ "capability": "record_decision", "payload": { "title": "…", "kind": "operational", "rationale": "…" }, "idempotency_key": "optional-stable-string"}Response:
{ "action_id": "act_01…", "verdict": { "result": "auto_approved", "reason": "capability default = auto" }, "requires_approval": false, "result": { "decision_id": "dec_01…" }}Verdict values: auto_approved, auto_approved_yolo, requires_approval, rejected.
GET /v1/me/substrate/actions?status=executed&capability=send_email_draft&limit=25&before=2026-05-28T00:00:00Z| Query param | Type | Default |
|---|---|---|
status | proposed | executed | rejected | all |
capability | string | all |
limit | int (1–500) | 100 |
before | ISO timestamp | now |
source_app_id | string | all |
source_rule_id | string | all |
Approve / reject
Section titled “Approve / reject”POST /v1/me/substrate/actions/{action_id}/approvePOST /v1/me/substrate/actions/{action_id}/reject{ "reason": "policy mismatch" }Both return the updated action row. Approving or rejecting an action that is not in proposed status returns 409 wrong_status.
Entities
Section titled “Entities”| Method | Path | Purpose |
|---|---|---|
| GET | /v1/me/substrate/entities | List entities |
| GET | /v1/me/substrate/entities/{entity_id} | Fetch one entity |
| PATCH | /v1/me/substrate/entities/{entity_id} | Update an entity (routed through propose) |
GET /v1/me/substrate/entities?type=person&q=alice&limit=20&count=true| Query param | Type | Default |
|---|---|---|
type | person | company | fund | workspace | team | project | event | agent | self | all |
q | string (display-name search) | none |
limit | int (1–200) | 50 |
count | true to include total in response | false |
Memory search
Section titled “Memory search”Full-text search across decisions, commitments, learnings, and principles.
GET /v1/me/substrate/memory/search?q=billing&kinds=decisions,commitments&limit=20Response:
{ "results": [ { "id": "dec_01…", "kind": "decision", "title": "Adopt substrate", "body_text": "agent memory needs a single source of truth", "rank": 0.18, "updated_at": "2026-05-31T…" } ]}Daily snapshots
Section titled “Daily snapshots”Snapshots are the basis for attention-rule snapshot_predicate conditions.
GET /v1/me/substrate/snapshots?days=7Response:
{ "snapshots": [ { "snapshot_date": "2026-05-31", "entity_count": 12, "decision_count": 8, "…": "…" } ]}Attention rules
Section titled “Attention rules”| Method | Path | Purpose |
|---|---|---|
| GET | /v1/me/substrate/attention-rules | List rules |
| GET | /v1/me/substrate/attention-rules/{rule_id} | Fetch one rule |
| POST | /v1/me/substrate/attention-rules | Create a rule |
| PUT | /v1/me/substrate/attention-rules/{rule_id} | Update a rule |
| DELETE | /v1/me/substrate/attention-rules/{rule_id} | Delete a rule |
| POST | /v1/me/substrate/attention-rules/{rule_id}/enable | Enable |
| POST | /v1/me/substrate/attention-rules/{rule_id}/disable | Disable |
| POST | /v1/me/substrate/attention-rules/preview | Dry-run a rule body against today’s snapshot |
| GET | /v1/me/substrate/attention-rules/{rule_id}/firings | List firings |
Rule body
Section titled “Rule body”{ "name": "weekly digest", "description": "Monday morning summary", "trigger_cron": "0 9 * * 1", "condition_mode": "snapshot_predicate", "condition": { ">": [ { "var": "entity_count" }, 0 ] }, "action_capability": "send_email_draft", "action_payload_template": { "to": "you@example.com", "subject": "Weekly digest", "body": "{{entity_count}} entities." }, "enabled": true, "max_fires_per_day": 1}| Field | Required | Notes |
|---|---|---|
name | yes | Display name. |
trigger_cron | yes | Standard 5-field cron expression (UTC). |
condition_mode | yes | snapshot_predicate evaluates a JSON-Logic predicate against today’s snapshot. row_query runs the condition as a row-query (advanced). |
condition | yes | JSON-Logic object. Available variables depend on condition_mode. |
action_capability | yes | The capability to propose when the rule fires. |
action_payload_template | yes | Object with {{var}} placeholders interpolated from the matched binding. |
enabled | no | Defaults to true. |
max_fires_per_day | no | Caps daily proposals. |
Preview
Section titled “Preview”POST /v1/me/substrate/attention-rules/preview{ <same shape as rule body, name optional> }Response:
{ "bindings_count": 3, "sample_proposals": [ { "binding": { "entity_count": 12 }, "rendered_payload": { … }, "would_require_approval": false } ], "skip_reason": null}skip_reason is set (and bindings_count is 0) when the snapshot is missing or the condition can’t be evaluated.
Outbox targets
Section titled “Outbox targets”When an action with capability X executes, the substrate POSTs the rendered payload to the registered outbox target for X (if any). Deliveries are HMAC-signed and retried with backoff.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/me/substrate/outbox-targets | List all targets |
| PUT | /v1/me/substrate/outbox-targets/{capability} | Register or replace a target |
| DELETE | /v1/me/substrate/outbox-targets/{capability} | Remove the target |
PUT /v1/me/substrate/outbox-targets/send_email_draft{ "webhook_url": "https://example.com/hooks/substrate", "signing_secret": "min-8-chars", "source_app_id": "app_optional_scope"}source_app_id is optional; when set, the target only fires for actions proposed by that app.
Webhook delivery
Section titled “Webhook delivery”POST https://example.com/hooks/substrateContent-Type: application/jsonX-Butterbase-Signature: sha256=<hex>X-Butterbase-Delivery: <uuid>
{ "action_id": "act_01…", "capability": "send_email_draft", "payload": { … the rendered action payload … }, "executed_at": "2026-05-31T…"}Verify the signature with HMAC-SHA-256 over the raw body using signing_secret.
WebSocket stream
Section titled “WebSocket stream”Live push of every change to the caller’s substrate.
GET /v1/me/substrate/streamBrowser flow
Section titled “Browser flow”Browsers can’t send custom headers on a WebSocket upgrade, so the stream accepts a one-shot ticket.
-
Mint a ticket (Cognito or
bb_sub_Bearer):POST /v1/me/substrate/ws-ticket{ "ticket": "wst_…", "expires_in": 60 } -
Open the WS with
?ticket=:wss://api.butterbase.ai/v1/me/substrate/stream?ticket=wst_…
Tickets are single-use and expire after 60 seconds. Reused or expired tickets close the WS with code 1008 unauthenticated.
Server-side flow
Section titled “Server-side flow”Programmatic / server clients can put the substrate-scoped key in the Authorization header on the upgrade:
GET /v1/me/substrate/streamUpgrade: websocketAuthorization: Bearer bb_sub_…Or as a fallback query string:
wss://api.butterbase.ai/v1/me/substrate/stream?token=bb_sub_…Frames
Section titled “Frames”// First frame on open:{ "type": "hello", "ts": 1780198304 }
// Subsequent frames, one per change:{ "tbl": "action_ledger", "op": "insert", "id": "act_…", "user": "…" }{ "tbl": "entities", "op": "update", "id": "ent_…", "user": "…" }{ "tbl": "attention_rules", "op": "update", "id": "rule_…", "user": "…" }{ "tbl": "attention_rule_firings", "op": "insert", "id": "fire_…", "user": "…" }The stream does not include payloads — clients are expected to re-fetch the affected row by id.
Close codes
Section titled “Close codes”| Code | Meaning |
|---|---|
| 1000 | Normal close (initiated by client) |
| 1008 | Ticket missing, expired, reused, or token rejected |
SDK / CLI
Section titled “SDK / CLI”- CLI: see
butterbase substratefor every command. - TypeScript SDK: substrate calls are namespaced under
butterbase.substrate.*(mirror of the HTTP surface). - Inside a function:
ctx.substrate.*(propose,getEntity,findEntities,searchMemory) — see Substrate.