# Workers

> Claim jobs and report on them over HTTP, for a worker in any language.

A worker does two things: it **claims** jobs, and it **reports** on each job it holds, until the job is done. The SDKs' workers do both for you, renewing leases and retrying reports. Use these endpoints to write a worker in a language without an SDK.

Both need a token with the `work` scope.

## Claim jobs

`POST /v1/spaces/{space}/jobs:claim` · scope `work`

Hands out up to `count` jobs, oldest first. With `waitMs`, the call waits for work to arrive instead of answering empty at once.

:::fields
:::field count {type="integer" required}
How many jobs you can take, from 1 to 1,000. The answer also stops at 8 MiB of payloads.
:::
:::field names {type="string[]"}
Only jobs of these names, at most 32. Leave it out for any job in the space.
:::
:::field waitMs {type="integer"}
How long to wait for work when there's none, up to 60 seconds.
:::
:::field leaseMs {type="integer" default="each job's timeout"}
How long you hold each job before it goes to someone else, unless you renew it.
:::
:::

```bash
curl -s "$EMIT_URL/v1/spaces/emails/jobs:claim" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"names": ["send-welcome-email"], "count": 10, "waitMs": 30000}'
```

```json
{
  "jobs": [
    {
      "id": "01M3QSMMXQ00ZSGYA6VWM3NGQA",
      "attempt": 1,
      "name": "send-welcome-email",
      "payload": { "userId": "usr_42", "email": "ada@example.com" },
      "leaseUntilMs": "1790726572493"
    }
  ]
}
```

Keep each job's `id` and `attempt`: every report names both. An empty `jobs` means nothing arrived in time; ask again. Give your HTTP client a timeout longer than `waitMs`.

:::warning A claim whose answer is lost
If the connection drops after emit handed out jobs, nobody knows which ones. Those jobs wait until their leases run out, and each loses an attempt. Keep `count` to what you'll work on, and leases no longer than you need.
:::

## Report on jobs

`POST /v1/spaces/{space}/jobs:report` · scope `work`

Send one report, an array of them, or `{"reports": [...]}`. A report can renew the lease, carry progress, an event and log lines, and end the attempt, all at once.

:::fields
:::field id {type="string" required}
The job's id.
:::
:::field attempt {type="integer" required}
The attempt you were handed. A report for an attempt that isn't the current one is refused, so a worker that lost its lease can't speak for the one that holds the job now.
:::
:::field touch {type="object"}
Still working: renew the lease. `{"leaseMs": 60000}` for a minute from now, or `{}` for the job's timeout. A lease is never shortened this way.
:::
:::field complete {type="object"}
Done: `{"result": <any JSON>}`. The result is at most 4 MiB.
:::
:::field fail {type="object"}
This attempt failed: `error`, a message; `unrecoverable: true` to fail the job for good; or `retryAfterMs` to try again after that wait (at most a day) instead of the space's policy.
:::
:::field release {type="object"}
`{}`: give the job back unfinished. It's handed out again at once, and the attempt isn't counted.
:::
:::field progress {type="object"}
`percent` (0 to 100) and `message`. Only the latest is kept.
:::
:::field event {type="JSON"}
An event to keep with the job, at most 4 KiB.
:::
:::field lines {type="object[]"}
Log lines, up to 1,000 a report: `level` (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` or `FATAL`), `message` (at most 4 KiB), and optional `source` and `metadata` (any small JSON).
:::
:::

Give at most one of `touch`, `complete`, `fail` and `release`.

:::code-group
```bash Progress
curl -s "$EMIT_URL/v1/spaces/emails/jobs:report" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "01M3QSMMXQ00ZSGYA6VWM3NGQA", "attempt": 1,
    "touch": {},
    "progress": {"percent": 50, "message": "rendering"},
    "lines": [{"level": "INFO", "message": "template rendered"}]
  }'
```
```bash Complete
curl -s "$EMIT_URL/v1/spaces/emails/jobs:report" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id": "01M3QSMMXQ00ZSGYA6VWM3NGQA", "attempt": 1, "complete": {"result": {"messageId": "msg_5521"}}}'
```
```bash Fail
curl -s "$EMIT_URL/v1/spaces/emails/jobs:report" \
  -H "Authorization: Bearer $EMIT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id": "01M3QSMMXQ00ZSGYA6VWM3NGQA", "attempt": 1, "fail": {"error": "SMTP server timed out", "retryAfterMs": 30000}}'
```
:::

```json
{ "outcomes": [{ "status": "RUNNING" }] }
```

The request answers `200` with one outcome per report: the job's status after it, or a rejection. Only when every report in the request breaks a rule is the whole request refused, with `400`.

| `reason` | What it means |
| --- | --- |
| `STALE` | Another attempt holds the job now. Stop working on it. |
| `NOT_RUNNING` | The job isn't held by anyone. |
| `ALREADY_FINISHED` | It finished already, for example because it was cancelled. Stop working on it. |
| `NOT_FOUND` | No such job. |
| `TOO_MANY_EVENTS` | The job has all the events its space allows. The rest of the report was applied. |
| `INVALID` | The report breaks a rule, which `message` names. The other reports in the request were applied. |

## Keeping the lease

Renew the lease well before it runs out: the SDKs send a `touch` every half-lease. If a lease runs out, the attempt counts as failed and the job goes to another worker, even if the first one is still working. A later report from the first worker is answered `STALE`.

A job may be worked on twice this way, so make the work safe to repeat. See [Leases and timeouts](/docs/concepts/jobs#leases-and-timeouts).
