Skip to content

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.

FieldTypeDescription
operationstring | string[]Operation type: "add", "revise", "retract"
kindstring | string[]Entity kind: "shape", "thing", "assertion", "collection"
shapestringShape 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.
namestring | 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.
matchstring | 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.
FieldTypeDescription
allFilterNode[]AND — all children must match
anyFilterNode[]OR — at least one child must match
notFilterNodeNOT — child must not match

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 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" }
Terminal window
wh sub create shape-changes --repo myorg/myrepo \
--kind webhook \
--filter '{"kind":"shape"}' \
--webhook-url https://hooks.example.com/shapes

Subscribe to shape retracts only:

{
"all": [
{ "kind": "shape" },
{ "operation": "retract" }
]
}
Terminal window
wh sub create shape-retracts --repo myorg/myrepo \
--kind webhook \
--filter '{"all":[{"kind":"shape"},{"operation":"retract"}]}' \
--webhook-url https://hooks.example.com/shapes

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

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"}noTop-level exact "shape".
{"kind":["shape"]}noTop-level single-element array of "shape".
{"all":[{"kind":"shape"}, …]}noall chains preserve the shape-only guarantee.
{"any":[{"kind":"shape"}, {"kind":"thing"}]}yesany is not inspected for the exemption.
{"any":[{"all":[{"kind":"shape"}, …]}, {"all":[{"kind":"shape"}, …]}]}yesEven when every branch is shape-only, top-level any disqualifies the filter. Express as one {"all":[{"kind":"shape"}, {"any":[…]}]} instead.
{"not":{"kind":"shape"}}yesNegation does not narrow to shape ops.
{"kind":["shape","thing"]}yesMixed-kind unions match non-shape ops.
Any filter without a kind:"shape" constraintyesStandard webhook subscription path.

The create surface rejects filters in the “yes” rows when --on (or filterJson.shape) is missing.

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.