Authoring Components
Directory structure
Section titled “Directory structure”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.
Step 1: Create component.json
Section titled “Step 1: Create component.json”{ "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.
Step 2: Create manifest.json
Section titled “Step 2: Create manifest.json”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.
Add shapes
Section titled “Add shapes”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.
Add actions
Section titled “Add actions”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.
Add environment variables
Section titled “Add environment variables”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" } }]Add subscriptions
Section titled “Add subscriptions”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).
Add seeds
Section titled “Add seeds”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.
Configure health and teardown
Section titled “Configure health and teardown”"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.
Step 3: Write your action script
Section titled “Step 3: Write your action script”Action scripts receive the trigger payload and have access to the wh CLI. A minimal example:
#!/usr/bin/env bashset -euo pipefail
# Read trigger payload from stdinPAYLOAD=$(cat)REPO=$(echo "$PAYLOAD" | jq -r '.payload.repo.orgName + "/" + .payload.repo.repoName')
# Extract data from the triggering thingURL=$(echo "$PAYLOAD" | jq -r '.payload.data.url')
# Do work...RESULT="Processed: $URL"
# Write result back to the repowh 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.
Step 4: Validate
Section titled “Step 4: Validate”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.jsonandmanifest.json
Step 5: Install and test
Section titled “Step 5: Install and test”# If your component has no subscriptions, install from a local path for developmentwh component install . --repo myorg/myrepo
# If your component declares subscriptions, install from a GitHub source insteadwh component install github.com/myorg/my-component --repo myorg/myrepo
# Set any required credentialswh credential set my-creds openai_key --value sk-...
# Check healthwh component doctor my-component --repo myorg/myrepo
# Trigger by creating datawh commit create --add test-input --shape FooInput \ --data '{"url": "https://example.com", "priority": 1}' \ --repo myorg/myrepo
# Check subscription logswh 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 withwh sub attempts <name> <commitId>to see exit codes and error categories.