Hand work to the owner's own agent
Most of what an app does is code. An agent task is the exception: work you describe in a sentence, done by the app owner’s own agent, on their own machine.
Declare a rule in x-homespun-manifest.agentTasks. When a row matching it is written, the relay
queues a task. The owner runs homespun work, which claims one at a time and pipes it to whatever
agent they use. Homespun queues, leases and records; it never executes.
That distinction is the point. It lets an app do things nobody wants to write a parser for, without Homespun running anyone’s code or holding anyone’s model credentials.
When to reach for it
Section titled “When to reach for it”When the work is easier to describe than to implement:
- read a photographed till receipt into line items
- summarise a long note into a one-line title
- classify a free-text entry into one of your categories
- pull the fields you want out of a pasted email
When not to: anything with a correct answer you could write down. A total that is the sum of a column is arithmetic, not judgement. Agent tasks cost a model call each, and code is both cheaper and right every time.
A rule
Section titled “A rule”{ "on": "create", "collection": "receipts", "when": { "field": "status", "equals": "unparsed" }, "taskType": "parse-receipt", "prompt": "The row holds a photographed till receipt as an attachment id in `photoId`. Read the total, the retailer and the date, and write one row into line_items for each item, each with a name and a price in cents. Then set this receipt's status to 'parsed'.", "reads": ["receipts"], "writes": ["line_items"]}on, collection and when are the same trigger grammar notify and webhooks use. taskType is
a short label a worker can route on without reading the prompt. prompt is the work.
Two fields worth understanding before you write one
Section titled “Two fields worth understanding before you write one”reads and writes are the whole security boundary. They name collections this same manifest
declares, and the relay mints a credential per claim carrying exactly that access, expiring with the
lease. A task cannot touch a collection you did not name, including one added to the app later.
writes implies read on the same collection, because a task amending a row has to see it.
delete is never granted. There is no deletes key. A task can add and amend; it cannot remove.
The prompt is trusted; the row is not. The prompt comes from the manifest the owner approved at
install, and it is the only part of the envelope a worker should follow. The row that triggered the
task arrives separately as context, and the envelope says in as many words that it is data rather
than instructions, because row content may have been typed by any user of the app, including an
anonymous one. Do not undo that by writing a prompt that defers to a field of the row.
A task’s own write-back is a machine write, and an ordinary rule does not fire on one. So the receipt parser above can safely write into a collection it also triggers on: its result does not queue another task.
That protection is switched off for any rule whose when tests author kind. {"authorKindNotIn": ["system"]} reads as “skip seed rows” and says nothing about machine writes, but it disarms the
defence completely, so deploy rejects a rule that tests author kind and writes into a collection any
rule fires on. Exclude the machine kinds explicitly if you want author filtering on a self-writing
rule:
{ "when": { "authorKindNotIn": ["system", "service", "hook"] } }There is also a per-app hourly cap on tasks created, as an independent backstop.
Running a worker
Section titled “Running a worker”homespun work --exec "claude -p"homespun work --exec ./parse-receipt.sh --once # one pass, for cronThe envelope arrives on the command’s stdin as one JSON line, carrying the prompt, the row context, the collections in scope, a short-lived credential and the API base. Everything the command needs to write results back is in what it was handed.
Exit 0 acks the task; any non-zero exit nacks it and records stderr as the reason. Nothing is read from stdout, so any program is a valid worker, including a shell script. There is no SDK to adopt and no assumption about which agent you use.
How a task reaches your worker
Section titled “How a task reaches your worker”Two ways, and your command cannot tell them apart. A pushed envelope is identical to a claimed
one, so nothing about --exec changes.
Pushed. The worker holds one socket for every app you own and tells the relay how many tasks it
can take, which is --max-concurrent. The relay sends at most that many, each already leased, and
the worker tops the number back up as each one finishes. No request is made to fetch the work, so a
task queued now starts now.
Polled. The worker also asks, every --poll-interval. This is the floor and it needs no
configuration: it covers a dropped socket, a relay with push switched off, a message lost in flight,
and anything queued while nothing was connected.
The two share one budget, so together they never run more than --max-concurrent children. If
you want tasks handled one at a time, --max-concurrent 1 means that on both paths.
Telling your user
Section titled “Telling your user”A page can ask about its own row:
GET /_hs/tasks?collection=receipts&row=r1It answers with a status per rule: queued, working, done, failed or expired. Only for rows
the caller can already read, and it never returns the prompt, the credential or the worker’s report.
Enough to say “still working on it”; not enough to leak anything.
Limits worth knowing
Section titled “Limits worth knowing”- A task only happens while a worker is running. With none, tasks wait and are expired after their TTL rather than piling up forever. This is not a relay-side job runner.
- Not available in published community templates. A template’s task would carry the publisher’s prompt, run against the installer’s data, on the installer’s machine, holding a credential the publisher scoped. An install cannot meaningfully consent to that, so publish rejects it.
- Editing a prompt does not repair a queued backlog. The prompt is part of the rule’s identity, so changing it makes a different rule and already-queued tasks are discarded rather than re-run under the new wording. Deliberate: a task queued against one instruction should not silently execute another against a row that has moved on.
The full field table, the envelope shape and the worker contract are in the Homespun skill’s
references/agent-tasks.md, which homespun skill show --section agent-tasks prints.