# 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-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”](#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”](#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.

## Loops

[Section titled “Loops”](#loops)

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”](#running-a-worker)

Terminal window

```
homespun work --exec "claude -p"
homespun work --exec ./parse-receipt.sh --once   # one pass, for cron
```

The 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”](#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”](#telling-your-user)

A page can ask about its own row:

```
GET /_hs/tasks?collection=receipts&row=r1
```

It 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”](#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.
