Trigger dispatch protocol
When the substrate decides an action should happen, Cortex HMAC-signs a JSON envelope and POSTs it to the customer-registered handler. You verify the signature, execute through your app's native channels, and call back. The full contract is specified by ADR-0019.
The shape of a decision
Five-step flow, end to end:
- You register a handler URL via
handler.register. Cortex returns a one-timewebhook_secretand fires a challenge POST you respond204to. - The substrate decides — trust threshold crossed, rule fired, commitment going stale, contradiction detected, pattern matched. The decision is appended to the ledger.
- Cortex builds the trigger envelope, HMAC-SHA256 signs it with your handler's secret, and POSTs to your URL with retries.
- Your handler verifies the signature, executes the action through whatever channel your app owns (Slack, email, in-app notification, calendar invite, anything HTTPS), and calls back
trigger.complete. - If the user reverses (undo within window, rejection, drift), your app calls back
trigger.reverse. Cortex demotes trust for the (action class × counterparty) pair automatically.
The trigger envelope
Documented in full at Envelope & headers. The headline shape:
interface TriggerEnvelope {
trigger_id: string; // ULID, idempotent across retries
decision_id: string; // ledger-linked decision
action_class: string; // "communicate" | "schedule" | "remind" | ...
mode: "act" | "notify_after" | "ask" | "test";
counterparty?: { id: string; display_name: string; kind?: string };
payload: Record<string, unknown>; // action-class-specific body
reasoning: string; // human-readable why
reversal: { path: string; window_sec?: number };
expires_at: string; // ISO-8601
schema_version: string; // "2026-07-20" at launch
}Signing & verification
HMAC-SHA256 over the canonical request string. Headers Cortex sets on every dispatch:
X-Cortex-Signature: sha256=<hex(hmac_sha256(secret, payload))>
X-Cortex-Trigger-Id: trg_01HXP9...
X-Cortex-Idempotency-Key: idk_01HXP9...
X-Cortex-Schema-Version: 2026-07-20
X-Cortex-Timestamp: 2026-05-27T14:31:00Z
Content-Type: application/jsonCanonical signing string: timestamp + "." + body (5-minute replay window). Reject any request older than 5 minutes from X-Cortex-Timestamp. A full TS / Python / Express verifier lives in the custom-webhook recipe; per-platform verifiers are linked from each recipe page.
Delivery semantics
| Property | Behavior |
|---|---|
| Delivery | At-least-once. Dedupe on X-Cortex-Idempotency-Key in your handler. |
| Response timeout | 30 seconds. Any later response is treated as a transient failure and retried. |
| Retry policy | Exponential backoff (1s, 5s, 30s, 2m, 10m, 1h, 6h, 24h). Up to 8 attempts before DLQ. |
| Success criteria | 2xx response within 30s, OR an explicit trigger.complete callback (async confirmation). |
| Dead-letter queue | Triggers exceeding retry budget land in the DLQ, queryable via trigger.history and replayable via trigger.replay. |
| Handler health | 5 consecutive DLQ entries auto-disables the handler. Re-enable via handler.update. |
Callbacks — closing the loop
Documented in full at Callbacks. Two endpoints, both REST (not MCP tools), both require an Authorization: Bearer header with your principal token:
POST https://mcp.cortex.jakeselby.com/v1/triggers/{trigger_id}/complete
POST https://mcp.cortex.jakeselby.com/v1/triggers/{trigger_id}/reversetrigger.complete records the outcome (executed / approved / rejected / deferred) and any external reference (Slack message id, email message id, calendar event id) into the ledger. trigger.reverse records the reversal and automatically demotes trust if the reversal pattern matches a confidence-shaking heuristic.
Three handler tiers
| Tier | Mechanism | Status |
|---|---|---|
| 1 — HTTPS webhook | Customer hosts any HTTPS endpoint; Cortex POSTs the envelope. Works with Trigger.dev, Inngest, n8n, Zapier, your own Express/Hono/FastAPI, anything that accepts an HTTP request. Default tier and the MVP path. | July 20, 2026 |
| 2 — MCP-executor | Customer hosts an MCP server; Cortex acts as MCP client and invokes a tool on the customer's server. Same envelope as Tier 1, transported over MCP tools/call instead of HTTP POST. Documented in ADR-0019; implementation deferred to V1.1. | V1.1 |
| 3 — Cortex-hosted generic channels | Cortex-hosted email relay / push notifications / SMS for indie builders who don't want to host execution. Out of scope. Adding generic connectors turns the substrate into a competing app platform. See ADR-0019's cut-decisions section; covered today by recipes for managed workflow platforms (Trigger.dev, n8n, Zapier). | Out of scope |
Cross-app routing
When a single end-user has registered multiple AI products on the substrate — your sales tool, a scheduling tool, an inbox triage tool, all built on Cortex — the substrate has to decide which handler receives the dispatch. The routing tools let the user (or the customer app) record preferences per (user × action class) pair, and the substrate resolves the right handler at dispatch time.
Internal vs public surface (ADR-0020)
Two MCP endpoints exist in the Cortex codebase. They are not the same surface:
- Public —
https://mcp.cortex.jakeselby.com/mcp, served by@cortex/mcp-server. Everything in these docs. Bearer auth in beta; OAuth in V1.0. - Internal —
apps/web /api/mcpin the monorepo, consumed only by the Cortex chat app's agent runtime. Rejects any bearer other thanMCP_SERVICE_TOKEN. Not a reference architecture; not documented publicly.
The Cortex chat app is a privileged internal consumer (founder dogfood). The public reference is the minimal example handler repo plus the recipes. ADR-0020 specifies the boundary.