Skip to content

Runners and auto-start

A runner is a small always-on service that turns a Truetask kickoff into real work. Truetask calls it at the agent's wake URL. The runner starts a coding CLI such as Claude Code or Codex, and the agent does the task through Truetask MCP as its own bot account.

A runner is plumbing, not a harness. It does not plan, review or retry the work. The workflow contract the agent reads tells it what to do, and people review the result through done proposals.

Truetask ships a reference runner in its repository at integrations/runner: one file of Node 20 with no dependencies, meant to be read and adapted. Other runners speak the same contract: a gateway, a CI job, an n8n workflow.

Two channels

Truetask talks to the outside world about agents over two separate channels, and they do different jobs.

Outbound webhooksWake URL
PurposeObservability: tell other systems what happenedDelivery: hand work to the agent's runner
Set up inThe Webhooks dialog, one subscription eachThe agent, under Auto-start
ReceiversAny numberExactly one per agent
EventsEvery subscribed workspace eventThis agent's work only
SignatureX-Truetask-Signature: sha256=... over the bodyX-Webhook-Signature-V2 over <timestamp>.<body>
The answerLogged, never readThe acknowledgement decides whether a kickoff counts as delivered
RetriesDurable queue per deliveryKickoffs retry until they expire; nudges are sent once

A typical setup uses both. The wake URL starts the agent. A webhook subscription on the agent lifecycle events posts progress to chat or a dashboard, and records what happened even while the runner is down.

See Webhooks for the observability side.

Setting one up

  1. Create the agent. Add it to the boards it should work. Turn on the guardrail that stops it completing tasks, so it has to propose done and a person approves.
  2. Give it a wake URL and a signing secret. On the agent, open Auto-start and set Webhook URL to the address the runner listens on. Under Signing secret, choose New, then Generate, name it and save it. The generated value is copied for you and never shown again.
  3. Mint its token. On the agent, turn on External access and create a write-scope token. It belongs to the bot, never to a person.
  4. Fill in the environment and run it. Point the runner's working folder at a plain folder that holds your checkout, not the checkout itself.
  5. Test it. Press Send a test call under Auto-start. Then kick the agent off on a throwaway task with powers set to wake only, and watch the runner log.

See Creating an agent for steps 1 to 3 in the UI.

Verifying a wake call

A wake call is a POST with a JSON body. When the agent has a signing secret it carries two headers.

text
X-Webhook-Timestamp: <unix seconds>
X-Webhook-Signature-V2: <hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>

The signed string is the timestamp, a dot, and the raw request bytes exactly as received. Verify before you parse the JSON, and compare in constant time.

js
import crypto from 'node:crypto';

function verify_wake(secret, timestamp, raw_body, signature, now_s = Math.floor(Date.now() / 1000)) {
	if (!/^\d+$/.test(timestamp ?? '') || !/^[0-9a-f]{64}$/i.test(signature ?? '')) return false;
	if (Math.abs(now_s - Number(timestamp)) > 300) return false;
	const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(raw_body).digest();
	return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}

Reject a timestamp more than 300 seconds away from your clock in either direction, and remember the signatures you accepted inside that window so a captured call cannot be replayed.

An agent with no signing secret gets unsigned calls. The reference runner refuses them, and yours should too. A handoff to a workflow step that has its own launch URL is signed with that step's secret instead of the agent's.

What a runner receives

EventWhat it means
agent_run.kickoffA kickoff reached its agent. The only payload carrying the task brief.
agent_run.handoffA workflow step became this agent's turn.
agent_run.stalledThe run went silent past the watchdog window. Adds minutes_silent.
agent_run.approvedA person approved the done proposal.
agent_run.returnedA person returned the done proposal with feedback.
agent_run.failedThe watchdog gave up on a stalled run.
agent_run.wake_testThe Send a test call button under Auto-start.

Every payload carries event and event_type with the same value; route on event_type. None of them contain a secret or a token.

The kickoff payload carries kickoff_id, card_id, card_title, board_id, workflow_id, agent, requested_by, kickoff_url and kickoff_text, the brief itself. A handoff names the agent differently: the one whose turn it is, and whose runner should act, is to_agent.

An agent can narrow which nudges its wake URL receives under Events: handoff, stalled, approved, returned and failed. The kickoff and the test call are delivery, not observation, and always arrive.

For every event except the kickoff, read the task again before acting.

The acknowledgement

Answer a kickoff with a 2xx status and a JSON body.

json
{ "accepted": true, "runner": "reference", "eta_s": 300, "external_id": "<your job id>" }
  • accepted says whether the runner took the work.
  • runner names the runner for the people reading the kickoff.
  • eta_s is a rough estimate, in seconds, of when the work starts.
  • external_id is the runner's own job id, so you can cross-reference its logs.

Truetask keeps the acknowledgement on the kickoff and shows it as "Picked up by" that runner, with the ETA. Only a literal accepted: false declines; a 2xx with an empty body counts as delivered without an acknowledgement, so older runners still work. Keep the body under 64 KB.

Answer accepted: false when you cannot take the work now. The kickoff then stays queued, names who declined, and is offered again a minute later until it expires. A declined kickoff does not fall through to another power: the agent's own runner has the last word. A non-2xx status, no answer within 10 seconds, or an unreachable URL also leaves it queued.

So answer fast. Acknowledge first and do the work afterwards, never inside the request. A delivered kickoff that no run picks up is delivered again after 10 minutes, so deduplicate on kickoff_id. Nudges are sent once and never retried.

Kickoff powers

A kickoff is delivered through one of the agent's powers, and the first reachable one wins, in this order.

  1. wake: the agent's wake URL, when it has one.
  2. desktop: the owner's Truetask Desktop, when it is online and its coding tool is signed in.
  3. truetask: a workspace AI turn as the agent.

powers narrows that list. powers: ["wake"] forces the wake URL: the kickoff never lands on the owner's desktop, and it waits in the queue while the runner is down or declines.

Every kickoff surface accepts it: the REST endpoint, the MCP tool agent_kickoff_push, the automation kickoff node's Powers setting, and a routine's own powers. Forcing wake from a schedule or an automation matters, because both run without a person watching. Without it, a kickoff that fires while the owner's laptop happens to be open lands on the laptop instead of the runner.

CLI forms

The reference runner uses only the headless forms Truetask has verified. The prompt always goes on stdin.

sh
claude --print --output-format json [--model <model>] [--permission-mode bypassPermissions]
codex exec --cd <workdir> --ephemeral --output-last-message <file> [--model <model>] [--dangerously-bypass-approvals-and-sandbox] -

For headless authentication, run claude setup-token once interactively as the user that runs the runner and put the printed token in CLAUDE_CODE_OAUTH_TOKEN, or use ANTHROPIC_API_KEY when you pay through the API. For Codex, run codex login once as the runner's user.

MCP registration differs by tool. Claude Code reads .mcp.json in its working folder. Codex registers once with codex mcp add truetask --url https://<workspace>/mcp --bearer-token-env-var TRUETASK_MCP_TOKEN and reads the token from that variable at run time.

Security

Task text is the prompt

Anyone who can edit a task the agent works writes part of its prompt. Spawn the CLI with an argument array and no shell, and put the prompt on stdin only, never in argv.

  • Bypass is opt-in. The CLIs' bypass flags let the agent run commands without asking. Treat that switch as running code for everyone who can edit a task. Run the runner in a container or a VM, as a dedicated OS user with no access to your home folder, SSH keys or cloud credentials, with a working folder that holds nothing but the work.
  • Two secrets, opposite directions. The wake secret proves to the runner that a call came from Truetask. The MCP token proves to Truetask that a call came from the agent. Never reuse one as the other, and keep the wake secret out of the CLI's environment.
  • Least privilege. The MCP token belongs to the agent's bot account, never to a person. Keep the bot on only the boards it should work, and deny completing tasks so it has to propose done.
  • The token file stays private. For Claude Code the token has to be on disk in .mcp.json. Write it at mode 0600, keep it outside the repository, and never commit it or bake it into an image.
  • Serve it over HTTPS. The signature stops forged calls, but the brief is readable in transit over plain HTTP.

Concurrency

Truetask caps how many runs an agent has in flight, one by default. A kickoff past the cap stays queued with the reason.

Run one runner per agent, each with its own wake secret, token and working folder, rather than pointing several agents at one runner. One runner holds one MCP token, which is one bot identity.

Rotating secrets

Truetask signs with one secret at a time, so rotate in two steps. Create the new value as a new signing secret but do not pick it on the agent yet. Then put the new value in the runner, restart it, and straight away pick the new signing secret on the agent. Between the restart and the switch, wake calls fail with 401; kickoffs are retried every minute, so they only arrive late. Finish with Send a test call, then delete the old secret.

MCP tokens overlap, so there is no gap: mint a new one, update the runner, restart it, confirm a job reaches Truetask, then revoke the old token.

If Truetask cannot reach you

A runner behind NAT or on a laptop can poll instead. Leave the agent's wake URL empty and do not force powers: ["wake"], because a forced kickoff with no wake URL expires.

A kickoff assigns the bot to the task whether or not any power delivers it, so the assignment is the signal. List the agent's open tasks with its own token every minute or two, skip the ones with a run in flight or a pending done proposal, fetch the brief from the kickoff endpoint, and run the CLI as above. Polling trades latency for reachability, and the queue and the retries live in your poller.

Truetask works the same on Truetask Cloud and on your own server. Pages and sections that apply to one model only are labelled.