Subscription Filter JSON
Webhook subscriptions require a filter that determines which write operations trigger the action. Filters are evaluated against each operation; the subscription fires if at least one operation matches. Set a filter with --filter (CLI), filterJson (SDK/MCP) — see Creating Subscriptions for the create flow.
Predicates
Section titled “Predicates”| Field | Type | Description |
|---|---|---|
operation | string | string[] | Operation type: "add", "revise", "retract" |
kind | string | string[] | Entity kind: "shape", "thing", "assertion", "collection" |
shape | string | Shape name of the thing or assertion the operation targets. Filters remain stable if the shape is renamed later. Does not match shape-lifecycle operations — use {"kind":"shape"} plus name for those. |
name | string | string[] | Exact match on the operation’s name. For thing and assertion operations the name is the full Shape/name wref (e.g. Signal/sensor-1); for shape lifecycle operations it is the bare shape name (e.g. Reviewer). Most useful with kind: "shape" to target a single shape’s lifecycle (e.g. {"all":[{"kind":"shape"},{"name":"Reviewer"}]}). Array form is OR-combined and rejected at create time if it contains non-string entries or is empty. For things/assertions, prefer match for glob patterns. |
match | string | string[] | Glob match on the operation’s name. For things and assertions that name is the full Shape/name wref; for shape lifecycle operations it is the bare shape name (e.g. Reviewer, not Shape/Reviewer). Uses the same glob syntax as wh thing list --match. Array form is OR-combined and must contain at least one pattern. |
Boolean Combinators
Section titled “Boolean Combinators”| Field | Type | Description |
|---|---|---|
all | FilterNode[] | AND — all children must match |
any | FilterNode[] | OR — at least one child must match |
not | FilterNode | NOT — child must not match |
Examples
Section titled “Examples”Match all operations on a shape (simplest filter):
{ "shape": "Signal" }Match only new things added to a shape:
{ "all": [ { "operation": "add" }, { "kind": "thing" } ]}Match assertions or things but not revisions:
{ "all": [ { "any": [{ "kind": "assertion" }, { "kind": "thing" }] }, { "not": { "operation": "revise" } } ]}Shape Lifecycle Subscriptions
Section titled “Shape Lifecycle Subscriptions”Shape lifecycle subscriptions watch for shape adds, revises, and retracts rather than operations on things of a shape. They do not require an --on <shapeName> target; the filter drives matching.
Subscribe to all shape lifecycle events in a repo:
{ "kind": "shape" }wh sub create shape-changes --repo myorg/myrepo \ --kind webhook \ --filter '{"kind":"shape"}' \ --webhook-url https://hooks.example.com/shapesSubscribe to shape retracts only:
{ "all": [ { "kind": "shape" }, { "operation": "retract" } ]}wh sub create shape-retracts --repo myorg/myrepo \ --kind webhook \ --filter '{"all":[{"kind":"shape"},{"operation":"retract"}]}' \ --webhook-url https://hooks.example.com/shapesSubscribe to events on a specific shape by name:
{ "all": [ { "kind": "shape" }, { "name": "Reviewer" } ]}Webhook event discriminator: Shape adds and revises emit warmhub.write; shape retracts emit warmhub.retract. Branch on matchedOperations[*].operation.kind to distinguish shape lifecycle deliveries from thing/assertion deliveries.
shape vs. name for lifecycle filters: The shape predicate matches things and assertions whose shape is Reviewer, so it never matches shape lifecycle operations (a shape add isn’t itself “of a shape”). To target a single shape’s lifecycle, combine {"kind":"shape"} with {"name":"Reviewer"} instead of {"shape":"Reviewer"}.
--on Signal does not subscribe to changes to the Signal shape itself. Binding a subscription to a shape with --on Signal (or shapeName: "Signal") scopes it to Signal’s things and assertions — adding, revising, or retracting the Signal shape definition will not fire it. To subscribe to shape lifecycle events, omit --on and use a {"kind":"shape"} filter.
When --on may be omitted
Section titled “When --on may be omitted”The --on / shapeName requirement is waived only when the filter has a literal kind: "shape" (or kind: ["shape"]) constraint at the top level or inside an all chain. The check is structural, not semantic — any disjunctions and not branches are not inspected even when every branch happens to be shape-only.
| Form | --on required? | Notes |
|---|---|---|
{"kind":"shape"} | no | Top-level exact "shape". |
{"kind":["shape"]} | no | Top-level single-element array of "shape". |
{"all":[{"kind":"shape"}, …]} | no | all chains preserve the shape-only guarantee. |
{"any":[{"kind":"shape"}, {"kind":"thing"}]} | yes | any is not inspected for the exemption. |
{"any":[{"all":[{"kind":"shape"}, …]}, {"all":[{"kind":"shape"}, …]}]} | yes | Even when every branch is shape-only, top-level any disqualifies the filter. Express as one {"all":[{"kind":"shape"}, {"any":[…]}]} instead. |
{"not":{"kind":"shape"}} | yes | Negation does not narrow to shape ops. |
{"kind":["shape","thing"]} | yes | Mixed-kind unions match non-shape ops. |
Any filter without a kind:"shape" constraint | yes | Standard webhook subscription path. |
The create surface rejects filters in the “yes” rows when --on (or filterJson.shape) is missing.
Glob Matching
Section titled “Glob Matching”Match things under a naming branch (everything under Sensor/hq/):
{ "all": [ { "kind": "thing" }, { "match": "Sensor/hq/**" } ]}Match by deeper glob patterns — globstars, single-segment wildcards, and brace expansion all work the same as wh thing list --match:
{ "all": [ { "kind": "thing" }, { "match": "Sensor/**/temp" } ]}Match any of several patterns (array = OR; empty arrays are invalid):
{ "all": [ { "kind": "thing" }, { "match": ["Sensor/hq/**", "Sensor/warehouse/**"] } ]}To require two patterns both match, nest under all instead of using an array — see Naming as Navigation for worked examples.
Shape names in filters are rename-safe: if a shape is renamed later, existing filters continue to match that shape.
For cross-repo subscriptions, shape names are resolved against the source repo’s namespace at creation time.
Hit a problem or have a question? Get in touch.