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 calendar source below reads through a Google Calendar connection, and new Google Calendar connections (and reconnects) are paused for now. Existing subscriptions stay stored but do not fire. This page describes the source as it works when the connector is available.
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. |
external_search | false | Market fan-outs. |
include_divergence | false | Divergence-map fusion on external reads. |
The retired write_outputs and write_memory knobs are refused like any other unknown key.
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 are not on the Amdahl MCP server's tool list: their only event source, Google Calendar, is retired, so no Subscription can fire. Use the REST endpoints above. Subscription reads are still 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 |