Skip to main content
A subscription is a saved query. You give it a filter, in the same grammar a query takes (see querying answers). When a document starts matching the filter you get a subscription.entered event, and when it stops matching you get subscription.exited. Events are pushed to a webhook endpoint, and every event is also in the events feed.
A filter means exactly what it means in a query, including the rule for missing fields: a comparison on a missing field is false, and NotEq and NotIn are true. So ["answers.a.p", "NotEq", 0.5] matches documents with no answer yet. Add ["answers.a.p", "Exists", true] to leave those out. Filters are fixed. To change one, create a new subscription and delete the old. A changed filter would have no honest edges: every document would have to be judged against it again, which is what a new subscription does, and a new name makes that plain.

Create, list and query

  • events defaults to ["entered"]. Add exited if you keep a list of the matching documents, or you will never hear that one left.
  • endpoint defaults to none: the events go to the events feed only. An endpoint’s namespace_prefix must cover the namespace, or every namespace of a template.
  • include names the attributes and answers to send with each event. The attributes and answers the filter reads are always sent. state never is.
  • bulk is summary (the default) or deliver. See bulk changes.
  • name is 1 to 128 letters, digits, _ and -. Creating a subscription again with the same name and the same settings returns the existing one, so a retried create is safe.
  • status is syncing until the subscription’s first evaluation in the namespace, then live, with live_at.
  • lag_ms is how long the oldest change the subscription hasn’t evaluated has waited. It is accurate only to about a minute: a quiet subscription records its progress about once a minute, so lag_ms reads 0 until a change has waited longer than that. It tells you the subscription is stuck, not that it is a few seconds behind. It is null while the subscription is syncing.
  • warnings lists problems such as judgment_inactive: a judgment the subscription reads or includes was deactivated or deleted. Its answers stop changing, so the subscription sees nothing new from it. The subscription is never paused or deleted for that.
  • update changes events, include, endpoint and bulk for the changes evaluated after it. It can’t change filters.
  • delete removes the definition, and the next evaluation drops the subscription’s matching set. Events recorded before then, and deliveries already queued, still go out.
  • The query runs the saved filter with your own rank_by, top_k, cursor and include, and is billed like any query. It takes a namespace, never a template prefix, and its body can’t have filters. Use it to seed a receiver, and after subscription.synced.

What is refused

  • invalid_request: a filter on freshness, or on state; a judgment that doesn’t exist or is computed on read (on_read), in the filter or in include; state in include; empty or repeated events; more than 24 fields, counting those the filter reads; a name the namespace already has or inherits; an endpoint that doesn’t exist or doesn’t cover the namespace; a 101st subscription in a namespace, counting those it inherits; and an update that sends filters. See limits.
  • plan_required: a subscription on a template prefix below the Team plan, as for templates.
  • unavailable (503, with Retry-After): a create or update that names an endpoint while the endpoint can’t be checked. Nothing was changed, so retry it.

Events are edges, not states

A subscription sends an event when a document’s match changes, never while it stays the same. As long as every change is live, the events for one document alternate: entered, exited, entered, and so on. A document that joined or left silently, in the first evaluation or in a bulk change, breaks that chain: it can send exited without an entered before it. Those changes are always announced by subscription.synced, and re-reading the members then puts a receiver right. An entered or exited event carries:
  • subscription, with its id, name and defined_on, the namespace or template prefix it was created on;
  • namespace, the namespace the document is in;
  • document, with its id, revision, incarnation and updated_at;
  • attributes and answers: those the filter reads and those named in include, as a get returns them;
  • cause: change for a write or a new answer, fanout, periodic, or, for bulk changes you asked to receive, backfill (with the backfill’s job_id) or resync;
  • sequence, below;
  • url, the document’s get, to fetch the rest with your own key;
  • truncated: a body is at most 64 KB, so past that attributes and answers are left out, truncated is true, and url has them.
exited also has a reason: no_longer_matches, with the current values, which show why the document left; or deleted, with only the document’s id and incarnation. A document created again with the same id is a new life, with a new incarnation, and enters like any new document. sequence is the number of the evaluation that recorded the event in the namespace. Every event of one evaluation has the same number, and a later evaluation has a higher one, with gaps. For one subscription and document, a higher sequence is newer, so a receiver drops an event older than what it has. A document deleted and created again within one evaluation sends exited and entered with the same sequence; the incarnation tells them apart. The numbers start again when a namespace is deleted and created again, so forget what you kept for a namespace when you delete it.

Only settled answers count

A document is evaluated only once every answer its filter reads is up to date with its current revision. So a write never makes a document drop out on a pending answer and come back when the answer lands. A filter over two judgments waits for both. The freshness field can’t be used in a subscription’s filter, since only settled answers are seen. Only the answers the filter reads are waited for: an answer named only in include is sent as it stands, and can be pending. See freshness for how this fits with the freshness states. Events follow answer commits by a few seconds.

Bulk changes

A backfill, or an edit to a threshold your filter reads, can move thousands of documents at once, without anything happening to any of them. So by default, "bulk": "summary", those moves update the subscription without sending an event for each document. You get one subscription.synced event instead:
reason is one of:
  • created, once the subscription’s first evaluation in the namespace is done;
  • backfill, sent once an evaluation finds no more of the backfill’s answers, or 10 minutes after the first one, whichever comes first. Backfills that overlap share one synced, with all their job_ids;
  • thresholds_changed, when a threshold the filter reads is edited, or a new version changes it;
  • weights_changed, when a composite judgment the filter reads gets new calibration fits.
On synced, read the current members again with the subscription’s query. A subscription whose receiver must see every change sets "bulk": "deliver". It gets an event for every document a backfill or a definition change moves, with cause: "backfill" and the job_id, or cause: "resync". The first evaluation is silent either way. Answers from fan-outs and periodic runs are live changes, never bulk. When a document has both a live change and a bulk one at the same time, the live change wins.

A new subscription starts from now

A subscription is edge-triggered from when you create it. Its first evaluation in a namespace is silent, whatever bulk says: the documents that already match join without an event. Then one subscription.synced with reason: "created" says how many joined, even when none did, which tells you the subscription is live there. A document written or answered after the subscription was created is the exception: its change sends an event as usual, so the first change after creation is never lost. Suppose a document matched when you created the subscription and stopped matching before that first evaluation. It sends no exited, and it never sent an entered either. A receiver built from events and the synced re-reads stays consistent. A receiver that listed the matches with a query at creation time does not.

Templates

A subscription created on a template prefix, such as acme/prod/*, applies to every namespace under it, including tenants created later. See templates.
  • Per tenant. Each tenant has its own matching set, and its events name the tenant in namespace. subscription.defined_on names the prefix the subscription was created on, so a platform routes events without a lookup.
  • Lazy start. A tenant starts evaluating a template’s subscription at its next change after the subscription exists, so idle tenants cost nothing. Until then the subscription reads syncing in that tenant.
  • No opting out. Tenants can’t change, delete or detach a template’s subscription. To leave one out, add a condition on id or an attribute to the filter.
  • Names. A tenant can’t create a subscription with the name of one it inherits. A template can create a name a tenant already has; both run, and the tenant’s own is listed first.
See multi-tenant platforms for setting up templates.

Limits

A namespace has at most 100 subscriptions, counting those it inherits from a template, on every plan. A subscription sends at most 24 attributes and answers, counting those its filter reads. Subscriptions and their events are never billed; the query is billed like any query. See limits and pricing.