Subscriptions
Event-fired Chats. A source watches your world - first source: upcoming Google Calendar events - and fires a fresh Chat a configurable lead time before each occurrence, with no human in the loop.
Put an agent behind an event — /subscriptions is event-fired Chats. A Subscription is the event-driven sibling of a Routine: where a routine fires on a cron, a subscription fires when its source observes an occurrence. Each fire starts one turn of Master — or the agent you pin — in a fresh Chat named "{Subscription name} — {date}", with the triggering event rendered into the turn so the agent knows exactly what woke it.
The first source is calendar.event_upcoming: fire a configurable lead time before each event on a connected Google Calendar. "Prep me 6 hours before every customer meeting, and again 10 minutes before" is two subscriptions on the same calendar connection — one per lead time, each with its own prompt, agent, and per-fire policy.
The model
The source observes
For calendar: one bounded window read per connection, shared across every subscription riding it.
sweep, every minute
A fire is planned
One row per (event, lead time), due at
max(now, start − lead). Structural dedupe: re-observing an unchanged event costs nothing.the fires ledger
The fire dispatches
A fresh Chat opens and one Master turn runs, headless —
on_questionis alwaysnone.when due
The ledger settles
Every planned occurrence records its outcome, so "did my trigger actually fire?" is one read.
fired / skipped / canceled
event observed (e.g. "Acme QBR", starts 15:00)
│
▼
fire planned: due_at = max(now, start − lead)
- discovered OUTSIDE the lead window → fires exactly at start − lead
- discovered already INSIDE the window → fires immediately
- start already passed → never fires
- rescheduled → the old occurrence's pending fires cancel; the new start plans fresh
- cancelled → pending fires cancel
│
▼
new Chat "Meeting prep — 2026-08-24"
│
▼
one Master turn, headless:
- prompt = the subscription's prompt + the event rendered as a readable block
- config = the subscription's run_config (depth, actions_allowed, …)
- on_question is ALWAYS "none"Every fire is a normal Chat run: watch it over the same SSE stream / read handles, find it in the Chats list (GET /chats?subscription_id=<id> scopes the list to one subscription's fires), and every Chat it starts carries a subscription trigger badge.
Connect a calendar first
The calendar source reads through a google_calendar connection (Connections → Google Calendar; OAuth, per-member — each member connects their own calendar). The connect flow requests offline access so the subscription sweep can keep reading after the initial sign-in. A subscription then names the connection it watches via config.connection_id, and may only name a connection in its own workspace.
Create a subscription
Read GET /subscriptions/kinds first — the source-kind catalog is self-describing: each kind ships its config fields (key, type, required, description), so the config you write matches what the kind expects.
curl -X POST https://app.amdahl.ai/api/platform/v1/subscriptions \
-H "X-API-Key: $AMDAHL_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Meeting prep",
"source_kind": "calendar.event_upcoming",
"prompt": "Prep me for this meeting: who is attending, what we last discussed with their company, open deals, and the three things to bring up.",
"config": {
"connection_id": "<google_calendar connection id>",
"lead_minutes": 360,
"require_attendees": true
},
"agent": "gtm-strategist"
}'Body fields
| Field | Required | What it does |
|---|---|---|
name | yes | Human label. Each fired Chat is named "{name} — {date}". |
source_kind | yes | Which source fires it (discover via GET /subscriptions/kinds). Immutable. |
prompt | yes | The Chat input every fire starts with; the event is appended as a readable block. |
config | yes* | Per-kind event config, validated against the kind's own schema (below). |
agent | no | Pin an agent: the fired Master turn runs AS that agent. null clears the pin. |
run_config | no | Per-fire Chat policy (below). |
enabled | no | Default true. false pauses without deleting. |
* Required in practice for calendar.event_upcoming (it declares required fields); a kind with no required fields accepts an empty config.
calendar.event_upcoming config
| Key | Required | Notes |
|---|---|---|
connection_id | yes | The google_calendar connection to watch. Must belong to this workspace. |
calendar_id | no | Google calendar id. Defaults to primary. |
lead_minutes | yes | Minutes before the event start to fire (360 = 6 hours, 10 = 10 minutes). One subscription = one lead. |
summary_contains | no | Only fire for events whose title contains this text (case-insensitive). |
require_attendees | no | Only fire for events with at least one attendee. |
Run config
The persisted per-fire Chat policy. on_question is not configurable — subscription fires are headless, so a human ask always fails fast with no_human_in_loop.
| Key | Default | Notes |
|---|---|---|
depth | standard | quick / standard / deep. A fire is a reaction to one event, so the default is the interactive tier. |
actions_allowed | (absent = all) | Outbound actions each fired run may invoke. Absent = every cataloged action; a list narrows, [] disables. |
write_outputs | false | Living-doc commits from fired runs. |
write_memory | false | Long-term memory commits. |
external_search | false | Market fan-outs. |
include_divergence | false | Divergence-map fusion on external reads. |
Manage subscriptions
| Verb | What it does |
|---|---|
GET /subscriptions/kinds | The source-kind catalog, each kind with its config fields. |
GET /subscriptions | List (filter ?source_kind= / ?enabled=, paginate ?limit=&offset=). |
GET /subscriptions/:id | One subscription, including the last fire's Chat + run ids and a derived source_health block. |
GET /subscriptions/:id/fires | The fire ledger: planned occurrences, due times, outcomes, event snapshots. |
POST /subscriptions | Create. |
PATCH /subscriptions/:id | Patch name / prompt / config / agent / run_config / enabled. |
DELETE /subscriptions/:id | Remove it and its fire history. Fired Chats are untouched. |
POST /subscriptions/:id/test-fire | Fire once now with a labeled synthetic event. Returns the Chat + run handles. |
test-fire returns the same { chat_id, run_id, stream_url, read_url, resume_url } envelope a Chat START does, so you can watch the fire live before trusting the source. A test fire is not written to the fires ledger — it is a preview, not an occurrence.
The fires ledger
GET /subscriptions/:id/fires is the answer to "is my trigger actually firing?". Each row is one planned occurrence:
| Status | Meaning |
|---|---|
pending | Planned, waiting for its due time. |
fired | Started a run — the row carries the chat_id + agent_run_id. |
skipped | Deliberately not run; skip_reason says why (subscription_disabled, rate_limited, agent_unresolvable, …). |
failed | The run could not start (start_failed), or a dead worker's claim was reaped. |
canceled | The occurrence no longer stands (event_canceled, event_rescheduled). |
Reliability semantics
- Fresh Chat per fire. A subscription never appends into an old Chat.
- One fire per occurrence, structurally. The ledger's dedupe key (for calendar: event + start time + lead) is UNIQUE, so sweep re-runs and concurrent workers cannot double-plan, and a claim race resolves to exactly one dispatcher.
- Reschedules re-fire; cancellations never fire. A rescheduled event is a new occurrence — its old pending fires cancel and the new start plans fresh. A cancelled event's pending fires cancel.
- Never after start. A fire is due at
max(now, start − lead)and an event whose start has passed never fires — a "prep me before the meeting" agent is never woken after the meeting began. - Rate-capped per workspace. Fires that start runs are capped per business per hour (default 30, operator-tunable), so a calendar with hundreds of imminent events cannot start hundreds of agent runs.
- A broken credential is a skip, not an outage — and a NAMED one. A connection whose Google credential fails skips its groups for the tick and is retried next tick; other connections are unaffected. The failure is classified and recorded: a definitive credential death (Google rejects the refresh with
invalid_grant, or no usable credential is stored) flips the connection toneeds_reauthon the Connections surface, while a transient Google error never does.
Source health
A quiet fires ledger has two very different causes — "no matching events" and "we cannot read the calendar" — and GET /subscriptions/:id tells them apart. The detail read carries a derived source_health block:
{
"status": "connection_needs_reauth",
"detail": "Google rejected the stored credential, reconnect Google Calendar under Connections, then fires resume automatically.",
"last_swept_at": "2026-08-25T12:00:00Z",
"checked_at": "2026-08-25T12:06:31Z"
}status | Meaning + remedy |
|---|---|
ok | The source is being read on schedule; last_swept_at is the last successful read. |
never_swept | No sweep has read this source yet — new subscriptions are picked up within about a minute. |
sweep_failing | The last read failed for a non-credential reason; retries continue automatically. |
connection_needs_reauth | The Google credential is dead — reconnect the connection under Connections; fires resume on their own. |
connection_unavailable | The named connection is missing, disconnected, or has not finished authorizing. |
config_invalid | The stored per-kind config no longer validates — edit the subscription. |
source_health can also be null, which means "not assessed" (the health lookup itself failed, or the source kind has no health resolver) — never a claim that the source is healthy. Recovery is automatic in both directions: a successful read clears a recorded failure, and a successful token refresh clears a needs_reauth stamp.
Over MCP
Subscriptions ride the Amdahl MCP server's agents tool (the same tool that carries the Chat lifecycle, the agent library, and Routines): list_subscription_kinds, list_subscriptions, get_subscription, list_subscription_fires, create_subscription, update_subscription, delete_subscription, and test_fire_subscription — the last returns the same Chat/run handles a start_chat does. Subscription reads are also MCP resources: subscription://kinds, subscription://list, subscription://<id>, and subscription://<id>/fires. Scopes: subscriptions:read / subscriptions:write (both on the customer-agent key bundle; existing keys were backfilled).
Scopes
| Scope | Grants | Default role |
|---|---|---|
subscriptions:read | kinds catalog, list, get, fire ledger | viewer |
subscriptions:write | create / update / delete / test-fire | editor |