Creating Subscriptions
Subscriptions are created with the wh sub create CLI command, the warmhub_subscription_create MCP tool, or the HTTP API. Each kind requires different configuration.
Trace Reentry
Section titled “Trace Reentry”Commit-triggered subscriptions (webhook and sprite) support an optional allowTraceReentry setting.
- Default:
false - CLI flag:
--allow-trace-reentry - Meaning when
false: a subscription runs at most once per trace per shape - Meaning when
true: the subscription may run again within the same trace - Hard fuse: a global chain-depth safety limit still applies even when reentry is allowed
This setting does not apply to cron subscriptions. If provided on cron create, it is ignored.
Current limitation: the MCP warmhub_subscription_create tool does not yet accept allowTraceReentry, so this option is currently available through the CLI, HTTP API, and typed backend surfaces only.
Scoped Sprite Tokens
Section titled “Scoped Sprite Tokens”Sprite executors can optionally define tokenScopes for the short-lived WarmHub token minted into each sprite run.
- Applies to any subscription that executes via
sprite - Webhook subscriptions ignore
tokenScopes - Token scopes are immutable after creation; change them by recreating the subscription
Use this when a sprite only needs a narrow slice of repo access instead of the full permissions implied by the subscription creator.
See Sprite Token Scopes below for the full field-level configuration details.
Cross-Repo Subscriptions
Section titled “Cross-Repo Subscriptions”A webhook or sprite subscription can watch a source repo that is different from the repo where the subscription lives. When a matching commit lands in the source repo, the subscription fires and runs in its home repo’s context.
Constraints
Section titled “Constraints”- The source repo must be in the same org as the subscription’s home repo. Cross-org source repos are rejected at creation time.
- Cron subscriptions cannot specify a source repo. Providing
--sourceon a cron create is an error. - The subscription creator must have read access to the source repo. Creation fails if this check does not pass.
allowTraceReentryapplies normally to cross-repo subscriptions.
Via CLI
Section titled “Via CLI”# Webhook subscription in myrepo that fires on commits to other-repowh sub create cross-hook \ --on Signal \ --kind webhook \ --source myorg/other-repo \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook
# Sprite subscription watching a source repowh sub create cross-sprite \ --on Signal \ --kind sprite \ --source myorg/other-repo \ --filter '{"shape":"Signal"}' \ --command 'bun run action' \ --input-mode stdinVia MCP
Section titled “Via MCP”Cross-repo subscriptions are not currently supported through the warmhub_subscription_create MCP tool. Use the CLI to create cross-repo subscriptions.
Via HTTP API
Section titled “Via HTTP API”Cross-repo creation via the HTTP API is not currently supported. Use the CLI to create cross-repo subscriptions.
Payload Differences
Section titled “Payload Differences”For cross-repo subscriptions, the sprite input payload includes a sourceRepo field alongside the existing repo field:
| Field | Description |
|---|---|
repo | The subscription’s home repo — where the subscription lives and runs |
sourceRepo | The repo where the triggering commit occurred |
When the subscription is not cross-repo, sourceRepo is absent. Webhook payloads do not include sourceRepo.
Webhook Subscriptions
Section titled “Webhook Subscriptions”A webhook subscription sends an HTTP POST to your URL when matching operations are committed.
Required: shape, filter, webhook URL.
Via CLI
Section titled “Via CLI”wh sub create signal-hook \ --on Signal \ --kind webhook \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook
# Optional: allow same-trace reentry for a self-chaining webhookwh sub create signal-loop \ --on Signal \ --kind webhook \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook \ --allow-trace-reentryVia MCP
Section titled “Via MCP”{ "name": "warmhub_subscription_create", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook", "kind": "webhook", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "webhookUrl": "https://example.com/hook" }}Via HTTP API
Section titled “Via HTTP API”POST /api/repos/myorg/myrepo/subsContent-Type: application/json
{ "name": "signal-hook", "kind": "webhook", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "webhookUrl": "https://example.com/hook"}Webhook Payload
Section titled “Webhook Payload”The POST body includes:
| Field | Description |
|---|---|
event | "warmhub.commit" or "warmhub.cron" |
traceId | Unique trace identifier for the event chain |
runId | Action run identifier |
repo | { "orgName", "repoName" } — the subscription’s home repo |
commit | Commit details (number, author, message, operation count) — null for cron |
matchedOperationIndexes | Indexes of operations that matched the filter |
matchedOperations | The matched operations with their details |
Headers include X-WarmHub-Idempotency-Key, X-WarmHub-Run-Id, and X-WarmHub-Attempt for deduplication and observability.
Sprite Subscriptions
Section titled “Sprite Subscriptions”A sprite subscription executes a command in a sandboxed remote environment when matching operations are committed.
Required: shape, filter, command, input mode. Optional: GitHub repo/ref for cloning.
Via CLI
Section titled “Via CLI”# With GitHub repo clonewh sub create sprite-hook \ --on Signal \ --kind sprite \ --filter '{"shape":"Signal"}' \ --github-repo myorg/action-handler \ --ref main \ --command 'bun run action' \ --input-mode stdin
# Without clone (command-only)wh sub create sprite-hook \ --on Signal \ --kind sprite \ --filter '{"shape":"Signal"}' \ --github-repo none \ --command 'echo "triggered"' \ --input-mode stdin
# Allow same-trace reentry for a sprite loop handlerwh sub create sprite-loop \ --on Echo \ --kind sprite \ --filter '{"shape":"Echo"}' \ --github-repo none \ --command 'bun run action' \ --input-mode stdin \ --allow-trace-reentry
# Mint a read-only token scoped to specific nameswh sub create signal-hook \ --on Signal \ --kind sprite \ --filter '{"shape":"Signal"}' \ --command 'bun run action' \ --input-mode stdin \ --token-scopes-json '[{"permissions":["repo:read"],"allowedMatches":["Signal/*"]}]'Sprite Configuration
Section titled “Sprite Configuration”| Field | Required | Description |
|---|---|---|
command | Yes | Shell command to execute |
inputMode | Yes | How matched commit data is delivered: stdin, json-file, or env |
githubRepo | No | GitHub repo to clone (owner/repo or none) |
ref | If cloning | Git ref to checkout (branch, tag, or SHA) |
spriteName | No | Custom execution environment name |
inputPath | No | File path when inputMode is json-file |
spritePool | No | Repo-scoped execution pool name |
tokenTtlMinutes | No | WarmHub token TTL in minutes (default 10, clamped to 5–60) |
When a GitHub repo is configured, the execution environment clones the repo at the specified ref before running the command. The working directory is set to the cloned repository root.
When no GitHub repo is configured (--github-repo none or omitted), the command runs in a persistent subscription workspace directory. This directory persists across runs on the same execution environment, so files written in one run are available in the next.
Sprite Token Scopes
Section titled “Sprite Token Scopes”Subscriptions executed by sprite can optionally restrict the minted WarmHub token:
- CLI: pass
--token-scopes-jsonwith a JSON array - MCP: pass
tokenScopesas an array of objects towarmhub_subscription_create - HTTP API: pass
tokenScopesin thePOST /api/repos/:orgName/:repoName/subsJSON body for subscriptions executed bysprite tokenScopesmust contain at least one entry- A permission may appear in only one entry across the whole array
Example MCP payload:
{ "name": "warmhub_subscription_create", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook", "kind": "sprite", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "spriteConfig": { "command": "bun run action", "inputMode": "stdin" }, "tokenScopes": [ { "permissions": ["repo:read"], "allowedMatches": ["Signal/*"] } ] }}Each tokenScopes entry has:
| Field | Required | Description |
|---|---|---|
permissions | Yes | Non-empty array of repo permissions. A permission may appear in only one entry across the full tokenScopes array. For valid permission values, see Personal Access Tokens |
allowedMatches | No | Array of globs matched against WarmHub wrefs, for example Signal/* or Opinion/round-1/**, that further restrict the entry |
Cron Subscriptions
Section titled “Cron Subscriptions”A cron subscription fires on a schedule and delegates to a webhook or sprite executor.
Required: cronspec, executor kind. Cron subscriptions are not bound to a shape and do not use filters.
Cron subscriptions do not support a source repo (--source). Providing a source repo on a cron create is rejected.
Via CLI
Section titled “Via CLI”# Cron with webhook executorwh sub create health-check \ --kind cron \ --cronspec "*/15 * * * *" \ --executor webhook \ --webhook-url https://example.com/health
# Cron with sprite executor and timezonewh sub create daily-sync \ --kind cron \ --cronspec "0 8 * * *" \ --executor sprite \ --timezone America/New_York \ --command "bun run sync" \ --input-mode stdinCron Schedule Rules
Section titled “Cron Schedule Rules”Cronspec uses standard 5-field Unix cron format:
┌───────────── minute (0–59)│ ┌───────────── hour (0–23)│ │ ┌───────────── day of month (1–31)│ │ │ ┌───────────── month (1–12)│ │ │ │ ┌───────────── day of week (0–7, 0 and 7 are Sunday)│ │ │ │ │* * * * *Minimum interval: 5 minutes. Cronspecs that fire more frequently are rejected.
Timezone: Optional IANA timezone string (e.g., America/New_York). Defaults to UTC.
6-field format: Supported, but the seconds field must be literal 0 (no sub-minute scheduling).
Valid examples:
| Cronspec | Meaning |
|---|---|
*/15 * * * * | Every 15 minutes |
0 8 * * * | Daily at 8:00 AM |
0 9 * * 1 | Every Monday at 9:00 AM |
0 */6 * * * | Every 6 hours |
Invalid examples:
| Cronspec | Reason |
|---|---|
* * * * * | Every minute — below 5-minute minimum |
*/2 * * * * | Every 2 minutes — below 5-minute minimum |
Filter JSON Reference
Section titled “Filter JSON Reference”Webhook and sprite subscriptions require a filter that determines which commit operations trigger the action. Filters are evaluated against each operation in a commit; the subscription fires if at least one operation matches.
Predicates
Section titled “Predicates”| Field | Type | Description |
|---|---|---|
operation | string | string[] | Operation type: "add", "revise" |
kind | string | string[] | Entity kind: "shape", "thing", "assertion", "collection" |
shape | string | string[] | Shape name(s) — resolved to internal IDs at creation time |
namePrefix | string | Prefix match on the operation’s thing name |
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" } } ]}Match things with a specific name prefix:
{ "all": [ { "kind": "thing" }, { "namePrefix": "Sensor/" } ]}Shape names in filters are resolved to internal IDs when the subscription is created. This makes filters rename-safe — if a shape is renamed, filters continue to work correctly.
For cross-repo subscriptions, shape names are resolved against the source repo’s namespace at creation time.
Workspace Policy
Section titled “Workspace Policy”By default, subscriptions only fire on baseline commits. Workspace commits are ignored, preventing draft activity from triggering production automations.
To include workspace commits, set the workspace policy when creating a subscription:
Via CLI
Section titled “Via CLI”wh sub create ws-monitor --on Signal --kind webhook \ --filter '{"shape":"Signal"}' --webhook-url https://example.com/hook \ --workspace-policy includeVia SDK
Section titled “Via SDK”await client.subscription.create({ orgName: 'acme', repoName: 'world', name: 'ws-monitor', shapeName: 'Signal', filterJson: { shape: 'Signal' }, kind: 'webhook', webhookUrl: 'https://example.com/hook', workspacePolicy: 'include',})| Value | Description |
|---|---|
ignore | Default. Only baseline commits trigger the subscription. |
include | Both baseline and workspace commits trigger the subscription. |
See Workspaces for more on the isolation model.
Credential Binding
Section titled “Credential Binding”Webhook subscriptions can use credential binding to inject authentication headers into delivery requests. This keeps secrets out of webhook URLs and subscription configuration.
- Create a credential set:
wh credential create webhook-keys- Set authentication keys:
# Bearer tokenecho "tok_secret" | wh credential set webhook-keys WEBHOOK_BEARER_TOKEN
# Or API keyecho "key_secret" | wh credential set webhook-keys WEBHOOK_API_KEY- Bind the credential set to a subscription:
wh sub bind signal-hook --credentials webhook-keysSupported Auth Methods
Section titled “Supported Auth Methods”| Key Name | Header Produced |
|---|---|
WEBHOOK_BEARER_TOKEN | Authorization: Bearer <value> |
WEBHOOK_API_KEY | X-API-Key: <value> (or custom header via WEBHOOK_API_KEY_HEADER) |
WEBHOOK_BASIC_USERNAME + WEBHOOK_BASIC_PASSWORD | Authorization: Basic <base64> |
WEBHOOK_SIGNING_SECRET | X-WarmHub-Signature (HMAC-SHA256) + X-WarmHub-Timestamp |
Credential resolution is best-effort — if a credential set is missing or revoked, the webhook is still delivered without auth headers.
Unbinding
Section titled “Unbinding”wh sub unbind signal-hookRemoving the credential binding stops auth headers from being injected on future deliveries.