# Runbook: Coda Contract Creation

How to operate the contract-creation workflow in `ows-coda`: turn it on, drive it locally,
execute for real in QA, watch what it did, and recover when something goes wrong.

- **Service:** `ows-coda` (`@coda/server-app`)
- **Code:** `apps/server/src/ai/workflows/contract-creation/` and `apps/server/src/routes/workflows.ts`
- **Concept:** [Learn: Coda Contract Creation](../../learn/coda/contract-creation.md)
- **Design:** [Coda TRD](../../technical-projects/coda/TRD.md)

## Feature flags

The workflow is additive and **off by default**. Two env vars control it
(`apps/server/.env`):

| Var | Default | Effect |
|-----|---------|--------|
| `CODA_WORKFLOWS_ENABLED` | `false` | When false, the `/api/v1/workflows/*` routes don't exist at all (404). When true, they mount inside the normal `/api` middleware (auth, tenancy, permissions, rate limiting). |
| `CODA_WORKFLOWS_DRY_RUN` | `true` | When true, **confirm returns the exact would-be GraphQL mutation payloads and writes nothing.** Set false to actually execute. |

**Real (non-dry-run) execution also requires `ENVIRONMENT=qa`.** The underlying write handler
refuses contract mutations in any other environment, so the safe states are:

| `ENABLED` | `DRY_RUN` | `ENVIRONMENT` | Result |
|-----------|-----------|---------------|--------|
| false | n/a | n/a | Routes absent |
| true | true | any | Full flow, **no writes** (payloads echoed) |
| true | false | `qa` | **Real contracts created in QA** |
| true | false | not `qa` | Confirm returns a handler error (QA-only gate) |

## Drive it locally (dev console)

When workflows are enabled outside production, Coda serves a static test page:

1. In `apps/server/.env` set `CODA_WORKFLOWS_ENABLED=true`, `CODA_WORKFLOWS_DRY_RUN=true`,
   and the dev auth bypass `CODA_DEV_AUTH_BYPASS=true` (so fetches work without an Auth0 token).
2. `pnpm dev` (backend on `http://localhost:8080`).
3. Open **`http://localhost:8080/workflows-dev`**, the "Contract-creation workflow, dev
   console". Paste memo text or drop a PDF, start, review the preview, amend in the missing IDs,
   and confirm. In dry-run you'll get the mutation payloads back.

The dev console is the manual test surface until the workflow is embedded in
`frontend-royalties`. In the React client the same flow is driven by the
`useContractWorkflow` hook (`apps/client/src/hooks/use-contract-workflow.ts`).

## The API

All routes are under `/api/v1/workflows/contract-creation` and require auth. A thread is only
visible to the identity that created it (others get 404, so existence is not leaked).

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/contract-creation` | **Start.** Body: one or more of `text`, `document`, `params`. Runs extract, validate, preview, then pauses. Returns `201` with the status view. |
| `GET` | `/contract-creation/:threadId` | Current state of a thread. |
| `POST` | `/contract-creation/:threadId/amend` | Body `{ patch: <draft fields> }`. Merge a field patch, re-validate, re-preview. |
| `POST` | `/contract-creation/:threadId/confirm` | Execute the previewed plan (or dry-run). Idempotent. |
| `POST` | `/contract-creation/:threadId/cancel` | Abandon the run. Idempotent. |

- **Thread IDs** look like `wf_<uuid>` and are returned by start.
- **`document`** is `{ name, mediaType, base64 }`. Supported `mediaType`: `application/pdf`,
  `text/plain`, `text/markdown`, `text/csv`, `application/msword`,
  `application/vnd.openxmlformats-officedocument.wordprocessingml.document`. `base64` is raw
  (no `data:` prefix), max ~34 MB (≈25 MB binary).

### Status values

| `status` | Meaning | Next step |
|----------|---------|-----------|
| `awaiting_confirmation` | Preview ready, draft is clean | confirm / amend / cancel |
| `needs_input` | Draft is blocking (missing fields or errors) | amend, then confirm |
| `executed` | Real contract created | terminal |
| `dry_run` | Dry-run completed; `execution.plannedMutations` holds the payloads | terminal |
| `failed` | Execution attempted and the write API returned an error | terminal (see below) |
| `cancelled` | Run abandoned | terminal |
| `not_found` | Unknown/expired thread, or not yours | n/a |

### A typical session (dry-run)

```bash
BASE=http://localhost:8080/api/v1/workflows/contract-creation

# 1. Start from a memo. Returns { threadId, status: "needs_input" | "awaiting_confirmation", preview, missingFields, ... }
curl -s -X POST "$BASE" -H 'Content-Type: application/json' \
  -d '{"text":"Distribution deal with ... 80/20 split in favour of the label ..."}'

# 2. Memos lack internal IDs, so amend them in (reviewer-supplied).
curl -s -X POST "$BASE/$THREAD/amend" -H 'Content-Type: application/json' \
  -d '{"patch":{"account_id":"12345","signing_entity_id":"678","run_controller_id":"90","default_term_attachments":["111"]}}'

# 3. Confirm. Dry-run echoes execution.plannedMutations; real run returns the created contract.
curl -s -X POST "$BASE/$THREAD/confirm" -H 'Content-Type: application/json' -d '{}'
```

## Monitor what it did

Every execution attempt writes one **audit record** before returning:

- **Structured log:** message `"workflow execution audit"` with `auditId`, `threadId`,
  `workflow`, `identityId`, `outcome`, `dryRun`, `error`. Grep these in the server logs or
  Langfuse for "who created what, and did it succeed."
- **Cache:** key `coda:wf:audit:<threadId>` (JSON, 30-day TTL) holds the full record including
  the exact confirmed `toolInput` and the handler result.
- **Checkpoints:** workflow state lives under `coda:wf:ckpt:*` (7-day TTL). A paused run that
  isn't confirmed within 7 days expires and becomes `not_found`.

Audit IDs are `audit_<threadId>`, so a thread maps to exactly one audit record.

## Run the extraction eval

Extraction is the only stochastic step, so it has its own metric. From the repo root:

```bash
pnpm eval:contract-extraction              # real Bedrock model (needs AWS creds)
pnpm eval:contract-extraction -- --fake    # offline harness self-test (expects score 1.0)
pnpm eval:contract-extraction -- --only=clean-single-term
pnpm eval:contract-extraction -- --threshold=0.85
```

It scores each fixture (default threshold **0.9**) on field accuracy and checks the validation
gate (expected missing fields / blocking). Exit code is 0 only if every fixture passes.
Fixtures live in `apps/server/evals/contract-extraction/fixtures/`. Run this after touching the
extraction prompt or the draft schema. For how the scoring and gate checks work and why they
raise reliability, see [Learn: Coda Evals](../../learn/coda/evals.md).

## Common failures

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `404` on every workflow route | `CODA_WORKFLOWS_ENABLED=false` | Enable the flag and restart. |
| Start returns `status: needs_input` | Memo lacked required fields/IDs (expected) | `amend` in `account_id`, `signing_entity_id`, `run_controller_id`, `lifecycle_term_start`, and `default_term_attachments`, then confirm. |
| Confirm returns `409 needs_input` | Tried to confirm a blocking draft | Amend the listed `missingFields` / `issues` first. |
| Confirm gives `status: failed`, error mentions **"only enabled in QA"** | `DRY_RUN=false` outside QA | Either set `DRY_RUN=true` or run in `ENVIRONMENT=qa`. |
| Confirm gives `status: failed`, "**At least one attachment must be specified**" | A term had no label/vendor ID | Supply `default_term_attachments` or per-term `attachments` and re-run as a **new** thread (a failed thread is terminal). |
| Confirm gives `status: failed`, "**Following attachment already exists**" | A label attached to two terms | Model the rate difference as conditions on one term. |
| `502` from start | Bedrock extraction errored (bad creds, oversized/garbled document) | Check AWS creds and the document; retry. |
| Amend/confirm/cancel returns `409` | Thread already terminal | Start a new thread; terminal runs can't be re-driven. |
| Thread disappeared (`404`) | Checkpoint TTL (7 days) elapsed, or not your identity | Start over. |

## "Rollback"

There is **no automatic undo** for a real (QA) execution: the four mutations create a real
contract. The workflow's safety is *before* the write, not after:

- A thread executes **at most once**; re-confirming never creates a duplicate.
- A **failed** execution is terminal: fix the inputs and start a **new** thread.
- To remove a contract created in error, use the Abacus UI or the standard contract-deletion
  path in QA. Coda does not delete contracts.

The strong recommendation for any change to the workflow is to verify end-to-end in **dry-run**
first (payloads are exact), then run **one** real contract in QA and inspect it in the Abacus UI
before relying on the change.
