The automation controller exposes an HTTP API on port 8081 (configurable). This spec defines all endpoints, request/response formats, and behavior contracts.
Status: Conceptual spec — implementation should conform to this document.
http://localhost:8081
These endpoints receive calls from Timberborn's HTTP Adapters when in-game signals change state.
Purpose: Notify the controller that adapter {name} switched ON.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Adapter name (URL-encoded). Must match the in-game adapter name. |
Request Body: None (ignored if present).
Response:
{
"status": "ok",
"adapter": "Weather Drought",
"state": true,
"timestamp": "2026-03-11T20:15:30.123Z"
}Status Codes:
| Code | Meaning |
|---|---|
| 200 | State updated successfully |
| 500 | Internal error processing the event |
Side Effects:
- Updates internal adapter state to
true - Runs the rules engine against ALL rules
- Fires any matching rule actions (lever toggles)
- Logs event to event history
Purpose: Notify the controller that adapter {name} switched OFF.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Adapter name (URL-encoded). |
Response:
{
"status": "ok",
"adapter": "Weather Drought",
"state": false,
"timestamp": "2026-03-11T20:15:30.123Z"
}Status Codes: Same as /on/{name}.
Side Effects: Same as /on/{name}, but sets state to false.
Purpose: Return current adapter states, lever states, and recent events. Primary data source for the dashboard.
Query Parameters: None.
Response:
{
"adapters": {
"Weather Drought": {
"state": true,
"last_changed": "2026-03-11T20:15:30.123Z"
},
"Water Depth Critical": {
"state": false,
"last_changed": "2026-03-11T20:10:00.000Z"
}
},
"levers": {
"Pump Station": {
"state": true,
"last_changed": "2026-03-11T20:15:30.456Z"
}
},
"events": [
{
"type": "adapter",
"name": "Weather Drought",
"state": true,
"timestamp": "2026-03-11T20:15:30.123Z"
},
{
"type": "rule",
"name": "Drought Water Emergency",
"triggered": true,
"timestamp": "2026-03-11T20:15:30.200Z"
},
{
"type": "lever",
"name": "Pump Station",
"action": "on",
"timestamp": "2026-03-11T20:15:30.456Z"
}
],
"mode": "webhook",
"uptime_seconds": 3600,
"rules_evaluated": 142,
"actions_fired": 23
}Response Schema:
| Field | Type | Description |
|---|---|---|
adapters |
object | Map of adapter name → state object |
adapters[name].state |
boolean | Current on/off state |
adapters[name].last_changed |
string (ISO 8601) | When state last changed |
levers |
object | Map of lever name → state object |
levers[name].state |
boolean | Last known lever state |
levers[name].last_changed |
string (ISO 8601) | When lever was last toggled |
events |
array | Last 20 events (most recent first) |
events[].type |
string | "adapter", "rule", or "lever" |
events[].name |
string | Name of the adapter/rule/lever |
events[].state |
boolean | New state (adapter events) |
events[].triggered |
boolean | Whether rule conditions matched (rule events) |
events[].action |
string | "on" or "off" (lever events) |
events[].timestamp |
string (ISO 8601) | When the event occurred |
mode |
string | Operating mode: "webhook", "polling", or "hybrid" |
uptime_seconds |
number | Seconds since controller started |
rules_evaluated |
number | Total rule evaluations since startup |
actions_fired |
number | Total lever actions fired since startup |
Purpose: Return event history with configurable limit.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Number of events to return (most recent first) |
Response:
{
"events": [
{
"type": "adapter",
"name": "Weather Drought",
"state": true,
"timestamp": "2026-03-11T20:15:30.123Z"
}
],
"total_stored": 100,
"returned": 50
}Response Schema:
| Field | Type | Description |
|---|---|---|
events |
array | Event objects (same schema as /api/state events) |
total_stored |
number | Total events in memory |
returned |
number | Number returned in this response |
Purpose: Health check endpoint for monitoring and Docker HEALTHCHECK.
Response:
{
"status": "healthy",
"mode": "webhook",
"uptime_seconds": 3600,
"game_reachable": true,
"adapters_tracked": 5,
"levers_tracked": 3,
"rules_loaded": 6
}Status Codes:
| Code | Meaning |
|---|---|
| 200 | Controller is running and healthy |
| 503 | Controller is degraded (e.g., game unreachable in polling mode) |
# Required
mode: "webhook" # "webhook" | "polling" | "hybrid"
rules: [] # Array of rule objects (see below)
# Optional (with defaults)
webhook_port: 8081 # integer, 1024-65535
game_api_url: "http://localhost:8080" # string, valid URL
polling_interval_seconds: 5 # integer, >= 1 (used in polling/hybrid mode)- name: "Rule Name" # string, required — used in logging and events
conditions: # object, required
operator: "AND" # "AND" | "OR" — default: "OR" if omitted
checks: # array, required, min 1 item
- adapter: "Name" # string, required — must match in-game adapter name
state: true # boolean, required — expected state to match
actions: # array, required, min 1 item
- lever: "Name" # string, required — must match in-game lever name
action: "on" # "on" | "off", requiredmodemust be one of:webhook,polling,hybridrulesmust be a non-empty array- Each rule must have
name,conditions, andactions - Each check must have
adapter(string) andstate(boolean) - Each action must have
lever(string) andaction("on"or"off") operatordefaults to"OR"if omitted- If
modeispollingorhybrid,game_api_urlmust be reachable (warn on startup, don't crash)
- On receiving
/on/{name}or/off/{name}:- Update adapter state immediately
- Evaluate ALL rules (not just rules referencing that adapter)
- For each rule where conditions match: execute all actions
- For each rule where conditions don't match: do nothing (don't reverse previous actions — rules should have explicit "resolved" counterparts)
- Log all state changes and rule evaluations
- Return 200 OK to the game (fast — don't block on lever API calls)
- On each poll interval:
- Fetch
GET /api/adaptersfrom game - Compare to previous known state
- For any state changes, process as if a webhook was received
- If game unreachable, log warning and retry next interval (don't crash)
- Fetch
- To switch a lever ON:
GET|POST {game_api_url}/api/switch-on/{url_encoded_name} - To switch a lever OFF:
GET|POST {game_api_url}/api/switch-off/{url_encoded_name} - If lever API call fails: log error, continue processing (don't crash, don't retry immediately)
- Track lever state internally based on actions sent (optimistic)
- Load and validate config
- If invalid config: exit with clear error message
- Start webhook server (if mode is
webhookorhybrid) - Perform initial state sync via polling
GET /api/adaptersandGET /api/levers(all modes) - If game unreachable on startup: log warning, start anyway, retry sync periodically
- Log configured rules and webhook URLs for in-game setup reference
- On SIGINT/SIGTERM: stop accepting webhooks, finish in-progress rule evaluations, exit cleanly
- No state persistence needed — state rebuilds from initial sync on restart
- Never crash on runtime errors — log and continue
- Game unreachable → retry on next poll/webhook (don't queue retries)
- Invalid webhook path → return 404
- Malformed adapter name → log warning, skip
The controller should include CORS headers to allow the dashboard (served from a different port) to access the API:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Spec version: 1.0 | March 2026