Dispatch

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:

  1. You register a handler URL via handler.register. Cortex returns a one-time webhook_secret and fires a challenge POST you respond 204 to.
  2. The substrate decides — trust threshold crossed, rule fired, commitment going stale, contradiction detected, pattern matched. The decision is appended to the ledger.
  3. Cortex builds the trigger envelope, HMAC-SHA256 signs it with your handler's secret, and POSTs to your URL with retries.
  4. 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.
  5. 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:

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

http
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/json

Canonical 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

PropertyBehavior
DeliveryAt-least-once. Dedupe on X-Cortex-Idempotency-Key in your handler.
Response timeout30 seconds. Any later response is treated as a transient failure and retried.
Retry policyExponential backoff (1s, 5s, 30s, 2m, 10m, 1h, 6h, 24h). Up to 8 attempts before DLQ.
Success criteria2xx response within 30s, OR an explicit trigger.complete callback (async confirmation).
Dead-letter queueTriggers exceeding retry budget land in the DLQ, queryable via trigger.history and replayable via trigger.replay.
Handler health5 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:

http
POST https://mcp.cortex.jakeselby.com/v1/triggers/{trigger_id}/complete
POST https://mcp.cortex.jakeselby.com/v1/triggers/{trigger_id}/reverse

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

TierMechanismStatus
1 — HTTPS webhookCustomer 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-executorCustomer 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 channelsCortex-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:

  • Publichttps://mcp.cortex.jakeselby.com/mcp, served by @cortex/mcp-server. Everything in these docs. Bearer auth in beta; OAuth in V1.0.
  • Internalapps/web /api/mcp in the monorepo, consumed only by the Cortex chat app's agent runtime. Rejects any bearer other than MCP_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.