Skip to content

Authoring Components

A component is a Git repository (or subdirectory) with this layout:

my-component/
warmhub/
component.json # Identity
manifest.json # Resource declarations
actions/
my-action/run.sh # Action scripts (referenced by manifest)

The warmhub/ directory is required. Action scripts can live anywhere in the repo — the manifest references them by path relative to the repo root.

{
"id": "com.example.MyComponent",
"name": "my-component",
"version": "1.0.0",
"description": "Watches for Foo things and processes them"
}

The id uses reverse-DNS format: lowercase domain segments followed by PascalCase name segments. This is the ownership key — it must be globally unique.

Start with the skeleton and add sections as needed:

{
"$schema": "https://warmhub.dev/schema/component-manifest.v1.json",
"component": {
"id": "com.example.MyComponent",
"name": "my-component",
"version": "1.0.0"
},
"shapes": [],
"credentials": [],
"actions": [],
"subscriptions": [],
"seeds": [],
"health": {},
"teardown": {}
}

The component section must match component.json. The $schema field enables IDE autocomplete.

Declare shapes for data your component creates or consumes:

"shapes": [
{ "name": "FooInput", "fields": { "url": "string", "priority": "number" } },
{ "name": "FooResult", "fields": { "summary": "string", "score": "number" } }
]

If your component only reacts to shapes that already exist in the target repo (created by another component or manually), you don’t need to declare them here.

Actions define what runs when a subscription fires:

"actions": [
{
"id": "process-foo",
"kind": "sprite",
"source": { "kind": "component-repo" },
"execution": { "program": "bash", "args": ["actions/process/run.sh"] },
"input": { "mode": "stdin" }
}
]

The current managed action runtime clones your component repo and runs the specified program. For public repos, this is all you need.

Private repos require authentication:

"source": {
"kind": "component-repo",
"auth": {
"credentialSet": "my-creds",
"key": "github_token"
}
}

When source.auth is set, the installer generates an authenticated clone wrapper automatically.

Inject the WarmHub token or credential values into your action:

"env": [
{ "to": "WH_AUTH", "from": "warmhubToken" },
{ "to": "OPENAI_KEY", "fromCredential": { "set": "my-creds", "key": "openai_key" } }
]

Wire actions to triggers:

"subscriptions": [
{
"name": "mc/on-foo-add",
"trigger": { "kind": "event", "shape": "FooInput" },
"action": "process-foo",
"credentials": ["my-creds"]
}
]

Convention: prefix subscription names with a short component abbreviation (e.g., mc/ for my-component).

Create initial data at install time:

"seeds": [
{
"kind": "thing",
"shape": "ComponentConfig",
"name": "my-component",
"data": { "version": "1.0.0", "enabled": true }
}
]

ComponentConfig is a built-in shape — you don’t need to declare it in shapes.

"health": {
"requires": {
"shapes": ["FooInput", "FooResult"],
"subscriptions": ["mc/on-foo-add"]
}
},
"teardown": {
"subscriptions": { "onDisable": "pause" }
}

health is accepted by the manifest schema, but the current wh component doctor implementation does not read health.requires.* yet. Today, doctor checks the shapes, subscriptions, credentials, and seeds declared elsewhere in the manifest.

Action scripts receive the trigger payload and have access to the wh CLI. A minimal example:

#!/usr/bin/env bash
set -euo pipefail
# Read trigger payload from stdin
PAYLOAD=$(cat)
REPO=$(echo "$PAYLOAD" | jq -r '.payload.repo.orgName + "/" + .payload.repo.repoName')
# Extract data from the triggering thing
URL=$(echo "$PAYLOAD" | jq -r '.payload.data.url')
# Do work...
RESULT="Processed: $URL"
# Write result back to the repo
wh assertion create \
--shape FooResult \
--about "FooInput/$(echo "$PAYLOAD" | jq -r '.payload.name')" \
--name "result-$(date +%s)" \
--data "{\"summary\": \"$RESULT\", \"score\": 0.95}" \
--repo "$REPO"

Available tools in the current runtime image: bash, jq, wh, git, curl, node, npm, npx, claude.

Authentication: When WarmHub mints a token for the action runtime, the runtime exports WH_TOKEN automatically before your command runs. If your script needs the token under a different env var name, add an explicit env mapping:

"env": [{ "to": "WH_AUTH", "from": "warmhubToken" }]

Then in your script, read $WH_AUTH directly if you want an alias instead of the default WH_TOKEN.

Terminal window
wh component validate .

Fix any errors before publishing. Common issues:

  • Missing cross-references (subscription references a nonexistent action)
  • Reserved env variable names (WH_TOKEN, PATH, etc.)
  • Component ID format (must be reverse-DNS)
  • Mismatched id/name between component.json and manifest.json
Terminal window
# If your component has no subscriptions, install from a local path for development
wh component install . --repo myorg/myrepo
# If your component declares subscriptions, install from a GitHub source instead
wh component install github.com/myorg/my-component --repo myorg/myrepo
# Set any required credentials
wh credential set my-creds openai_key --value sk-...
# Check health
wh component doctor my-component --repo myorg/myrepo
# Trigger by creating data
wh commit create --add test-input --shape FooInput \
--data '{"url": "https://example.com", "priority": 1}' \
--repo myorg/myrepo
# Check subscription logs
wh sub log mc/on-foo-add --repo myorg/myrepo
  • Avoid self-triggering: If your action writes to the same shape the subscription monitors, it will create an infinite loop. Write to a different output shape.
  • Use /tmp/ for state: The /tmp/ directory persists across runs on the same runtime container. Use it to cache data between invocations.
  • Prefix subscription names: Use a consistent prefix (e.g., rk/ for research-knowledge) to avoid collisions with other components.
  • Test with wh sub attempts: After triggering, check delivery status with wh sub attempts <name> <commitId> to see exit codes and error categories.