Skip to content

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.

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.

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.

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.

  • 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 --source on 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.
  • allowTraceReentry applies normally to cross-repo subscriptions.
Terminal window
# Webhook subscription in myrepo that fires on commits to other-repo
wh 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 repo
wh sub create cross-sprite \
--on Signal \
--kind sprite \
--source myorg/other-repo \
--filter '{"shape":"Signal"}' \
--command 'bun run action' \
--input-mode stdin

Cross-repo subscriptions are not currently supported through the warmhub_subscription_create MCP tool. Use the CLI to create cross-repo subscriptions.

Cross-repo creation via the HTTP API is not currently supported. Use the CLI to create cross-repo subscriptions.

For cross-repo subscriptions, the sprite input payload includes a sourceRepo field alongside the existing repo field:

FieldDescription
repoThe subscription’s home repo — where the subscription lives and runs
sourceRepoThe repo where the triggering commit occurred

When the subscription is not cross-repo, sourceRepo is absent. Webhook payloads do not include sourceRepo.

A webhook subscription sends an HTTP POST to your URL when matching operations are committed.

Required: shape, filter, webhook URL.

Terminal window
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 webhook
wh sub create signal-loop \
--on Signal \
--kind webhook \
--filter '{"shape":"Signal"}' \
--webhook-url https://example.com/hook \
--allow-trace-reentry
{
"name": "warmhub_subscription_create",
"arguments": {
"orgName": "myorg",
"repoName": "myrepo",
"name": "signal-hook",
"kind": "webhook",
"shapeName": "Signal",
"filterJson": { "shape": "Signal" },
"webhookUrl": "https://example.com/hook"
}
}
Terminal window
POST /api/repos/myorg/myrepo/subs
Content-Type: application/json
{
"name": "signal-hook",
"kind": "webhook",
"shapeName": "Signal",
"filterJson": { "shape": "Signal" },
"webhookUrl": "https://example.com/hook"
}

The POST body includes:

FieldDescription
event"warmhub.commit" or "warmhub.cron"
traceIdUnique trace identifier for the event chain
runIdAction run identifier
repo{ "orgName", "repoName" } — the subscription’s home repo
commitCommit details (number, author, message, operation count) — null for cron
matchedOperationIndexesIndexes of operations that matched the filter
matchedOperationsThe matched operations with their details

Headers include X-WarmHub-Idempotency-Key, X-WarmHub-Run-Id, and X-WarmHub-Attempt for deduplication and observability.

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.

Terminal window
# With GitHub repo clone
wh 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 handler
wh 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 names
wh 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/*"]}]'
FieldRequiredDescription
commandYesShell command to execute
inputModeYesHow matched commit data is delivered: stdin, json-file, or env
githubRepoNoGitHub repo to clone (owner/repo or none)
refIf cloningGit ref to checkout (branch, tag, or SHA)
spriteNameNoCustom execution environment name
inputPathNoFile path when inputMode is json-file
spritePoolNoRepo-scoped execution pool name
tokenTtlMinutesNoWarmHub 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.

Subscriptions executed by sprite can optionally restrict the minted WarmHub token:

  • CLI: pass --token-scopes-json with a JSON array
  • MCP: pass tokenScopes as an array of objects to warmhub_subscription_create
  • HTTP API: pass tokenScopes in the POST /api/repos/:orgName/:repoName/subs JSON body for subscriptions executed by sprite
  • tokenScopes must 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:

FieldRequiredDescription
permissionsYesNon-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
allowedMatchesNoArray of globs matched against WarmHub wrefs, for example Signal/* or Opinion/round-1/**, that further restrict the entry

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.

Terminal window
# Cron with webhook executor
wh sub create health-check \
--kind cron \
--cronspec "*/15 * * * *" \
--executor webhook \
--webhook-url https://example.com/health
# Cron with sprite executor and timezone
wh sub create daily-sync \
--kind cron \
--cronspec "0 8 * * *" \
--executor sprite \
--timezone America/New_York \
--command "bun run sync" \
--input-mode stdin

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:

CronspecMeaning
*/15 * * * *Every 15 minutes
0 8 * * *Daily at 8:00 AM
0 9 * * 1Every Monday at 9:00 AM
0 */6 * * *Every 6 hours

Invalid examples:

CronspecReason
* * * * *Every minute — below 5-minute minimum
*/2 * * * *Every 2 minutes — below 5-minute minimum

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.

FieldTypeDescription
operationstring | string[]Operation type: "add", "revise"
kindstring | string[]Entity kind: "shape", "thing", "assertion", "collection"
shapestring | string[]Shape name(s) — resolved to internal IDs at creation time
namePrefixstringPrefix match on the operation’s thing name
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" } }
]
}

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.

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:

Terminal window
wh sub create ws-monitor --on Signal --kind webhook \
--filter '{"shape":"Signal"}' --webhook-url https://example.com/hook \
--workspace-policy include
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',
})
ValueDescription
ignoreDefault. Only baseline commits trigger the subscription.
includeBoth baseline and workspace commits trigger the subscription.

See Workspaces for more on the isolation model.

Webhook subscriptions can use credential binding to inject authentication headers into delivery requests. This keeps secrets out of webhook URLs and subscription configuration.

  1. Create a credential set:
Terminal window
wh credential create webhook-keys
  1. Set authentication keys:
Terminal window
# Bearer token
echo "tok_secret" | wh credential set webhook-keys WEBHOOK_BEARER_TOKEN
# Or API key
echo "key_secret" | wh credential set webhook-keys WEBHOOK_API_KEY
  1. Bind the credential set to a subscription:
Terminal window
wh sub bind signal-hook --credentials webhook-keys
Key NameHeader Produced
WEBHOOK_BEARER_TOKENAuthorization: Bearer <value>
WEBHOOK_API_KEYX-API-Key: <value> (or custom header via WEBHOOK_API_KEY_HEADER)
WEBHOOK_BASIC_USERNAME + WEBHOOK_BASIC_PASSWORDAuthorization: Basic <base64>
WEBHOOK_SIGNING_SECRETX-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.

Terminal window
wh sub unbind signal-hook

Removing the credential binding stops auth headers from being injected on future deliveries.