# How-to: use AI tasks

**Goal:** summarize, extract structured data from, classify, or generate text
over your documents — asynchronously, with results written back as platform
data.

Assumes `$LYDIAN`, `$TENANT`, and `$KEY` from the [Quickstart](/docs/quickstart.html).
AI tasks are admin-plane (API key only), and **AI is opt-in per tenant**: the
operator or your admin binds an `ai-inference` provider first (see
[Connect email, calendar, and meetings](/docs/connect-email-calendar-meetings.html#provider-bindings)).
Without an enabled binding every submit is `403 FORBIDDEN`.

---

## 1. The model: submit, poll, read

```bash
curl -s -X POST "$LYDIAN/tenants/$TENANT/ai-tasks" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"kind": "summarize", "documentIds": ["'"$DOC"'"],
       "instruction": "Focus on decisions and open questions."}' | jq
```

```json
{"id": "0199…", "kind": "SUMMARIZE", "tier": "DEFAULT", "status": "QUEUED", "createdAt": "…"}
```

The response is `202`; the work runs on the platform's job runner. Poll
`GET …/ai-tasks/{id}` until `status` is `SUCCEEDED` or `FAILED`:

```json
{"id": "0199…", "status": "SUCCEEDED",
 "result": {"markdown": "## Decisions\n- …"},
 "usage": {"inputTokens": 1830, "outputTokens": 212}, "finishedAt": "…"}
```

`GET …/ai-tasks?status=&kind=` lists tasks; `DELETE …/ai-tasks/{id}` removes
a finished one; `DELETE …/ai-tasks` purges every finished task. A queued or
running task cannot be deleted — `409 STATE_CONFLICT`.

## 2. The four kinds

| `kind` | You send | `result` carries |
|---|---|---|
| `summarize` | `documentIds` (one or more), optional `instruction`, optional `outputDocument: {title, typeId?}` | `markdown`; plus `outputDocumentId` when you asked for a document |
| `extract` | `documentIds`, a JSON `schema` | `data` — JSON conforming to the schema |
| `classify` | `items` (strings or small objects), `labels`, optional `instruction` | `labels` — one label per item, from your set, in order |
| `generate` | `prompt`, optional `context` object | `text` |

**Validation happens at submit time.** Every document must exist in your
tenant, the schema must parse, and the documents' text must fit the
platform's byte budget — the `400 VALIDATION_FAILED` details name what is
wrong, so a bad request never burns a model call. Document text is taken from
markdown documents as-is and extracted from uploads (PDF, Office, images via
OCR) the same way search indexing does.

### Structured output is checked, then retried once

`extract` and `classify` ask the model for structured output and the
platform validates the answer against your schema (or label set) locally.
A non-conforming answer is sent back once with the violations; a second
failure marks the task `FAILED` with the reason in `error`. Schemas use the
common JSON Schema subset: `type`, `properties`, `required`,
`additionalProperties`, `items`, `enum`, `const`, `pattern`, min/max bounds
for numbers, strings, and arrays, and `anyOf` / `oneOf` / `allOf`.

```json
{"kind": "extract", "documentIds": ["…"],
 "schema": {"type": "object",
            "properties": {"attendees": {"type": "array", "items": {"type": "string"}},
                           "nextMeeting": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}},
            "required": ["attendees"], "additionalProperties": false}}
```

`classify` batches items per model call, so classifying a few hundred short
strings is one task, not hundreds.

### Writing a document

`summarize` with `outputDocument` writes the markdown as a new document —
of the Base type unless `typeId` names another — created **as the acting
person**, so that submit needs the `X-Acting-Person` header:

```bash
curl -s -X POST "$LYDIAN/tenants/$TENANT/ai-tasks" \
  -H "X-API-Key: $KEY" -H "X-Acting-Person: $PERSON" -H "Content-Type: application/json" \
  -d '{"kind": "summarize", "documentIds": ["'"$DOC"'"],
       "outputDocument": {"title": "Notes: kickoff"}}'
```

## 3. Tiers, not models

`tier` is `default` (summarization-grade) or `fast` (classification-grade,
cheaper). You never name a model; `GET …/ai-models` shows what each tier
resolves to for your tenant and whether the platform's own credential is in
play. Which vendor answers, and on whose account, is the provider binding's
business.

## 4. Metering

```bash
curl -s -H "X-API-Key: $KEY" "$LYDIAN/tenants/$TENANT/ai-usage?month=2026-09" | jq
```

Returns the month's task count and input/output tokens, overall and per kind.
Tokens are recorded on every task (`usage`) whether it succeeded or failed.

The same tokens also land on the platform-wide usage ledger as
`ai.input_tokens` / `ai.output_tokens` (`GET …/usage`), where a monthly
allowance may bound them: once a `BLOCK` allowance is used up, submits answer
`429 QUOTA_EXCEEDED` until the month resets — see
[Usage &amp; entitlements](/docs/usage-and-entitlements.html).

## 5. Failure semantics worth knowing

- The vendor rate-limiting the platform (`429`) **pauses** the task until the
  vendor's retry hint, without spending one of its attempts.
- A model refusal, or any other vendor `4xx`, fails the task immediately —
  retrying would not help.
- Transient errors retry three times with backoff, like every job.
- An active task blocks deleting the tenant; finished ones cascade.

## 6. Inside a workflow

The `ai_task` step runs any of the four kinds inline during a workflow run
and merges the result keys (`markdown`, `outputDocumentId`, `data`, `labels`,
`text`, plus `aiTaskId`) into the run context for the following steps — see
[Automate with workflows](/docs/automate-with-workflows.html). A vendor
rate-limit pauses the whole run rather than failing it.
