> ## Documentation Index
> Fetch the complete documentation index at: https://www.autumn.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Autumn AI (autumn.ai) is a people and company intelligence API, a separate company from useautumn.com (billing software); no billing-product routes exist here.
> Base URL https://api.autumn.ai. Auth on every request: `X-API-Key: <key>` or `Authorization: Bearer <key>`. Plain HTTP + JSON. Hosted MCP: https://api.autumn.ai/mcp. Schemas: https://api.autumn.ai/openapi.json. Every docs page is Markdown at its URL plus `.md`.

# Move a table

> The steps a coding agent follows to move one Clay table to Autumn, from export to cutover.

For the agent doing the move. You need `clay` (`@clay-run/cli`), `autumn` (`@autumnai/cli`),
`jq`, Python with `requests`, and `AUTUMN_API_KEY` set.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
  subgraph read[Read]
    direction TB
    A[1 Check tools] --> B[2 Export from Clay]
  end
  subgraph plan[Plan]
    direction TB
    C[3 Write mapping] --> D{User approves}
  end
  subgraph prove[Prove]
    direction TB
    E[4 Run a sample] --> F[5 Compare] --> G{User approves}
  end
  subgraph switch[Switch]
    direction TB
    H[6 Full run] --> I[7 Cut over]
  end
  read --> plan --> prove --> switch
```

Read from Clay only. Don't write to the CRM until step 7. Never print an API key.

## 1. Check tools

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
clay whoami && autumn whoami
clay tables list --limit 100 | jq -r '.data[] | "\(.id)  \(.name)"'
```

If either isn't signed in, ask the user to run `clay login` or `autumn login`. Ask which table
to move.

## 2. Export from Clay

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
TABLE=t_abc123
mkdir -p clay-export && cd clay-export
clay tables get "$TABLE" > table.json
clay tables columns get "$TABLE" > columns.json

: > rows.jsonl; cursor=""
while :; do
  page=$(clay tables rows list "$TABLE" --limit 100 ${cursor:+--cursor "$cursor"}) || exit $?
  echo "$page" | jq -c '.data[]' >> rows.jsonl
  cursor=$(echo "$page" | jq -r '.cursor // empty'); [ -z "$cursor" ] && break
done
cd ..
```

**If you get exit code `3` (`auth_forbidden`),** the Clay plan may not include table reads.
Don't work around it. Ask the user for a CSV export (**Actions → Export → Download CSV**)
and for each enrichment and AI column's purpose and prompt. Then turn the CSV into the same
files, so the scripts below work unchanged:

```python csv_to_export.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import csv, json, os, sys

rows = list(csv.DictReader(open(sys.argv[1], newline="", encoding="utf-8-sig")))
os.makedirs("clay-export", exist_ok=True)
columns = [{"id": name, "name": name} for name in (rows[0] if rows else [])]
json.dump({"data": columns}, open("clay-export/columns.json", "w"))
with open("clay-export/rows.jsonl", "w") as fh:
    for row in rows:
        cells = {k: {"status": "success", "value": v} for k, v in row.items() if v}
        fh.write(json.dumps({"cells": cells}) + "\n")
print(f"{len(rows)} rows")
```

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
python3 csv_to_export.py clay-table.csv
```

Use that row count wherever the steps below mention `rowCount`.

What you get:

| File | Holds |
| - | - |
| `columns.json` | Each column's `type` (`basic`, `action`, `source`) and its `settings`: `formulaText`, `actionKey`, `inputsBinding` (prompts) and `conditionalRunFormulaText` (run condition). Skip `system: true`. |
| `rows.jsonl` | One row per line, cells keyed by column ID. Only cells with `status: "success"` have a `value`. |

Bulk enrichment tables return only a 20-row sample (`truncated: true`); use the original input
list instead.

## 3. Write the mapping

Sort each column:

| Column | Group | Goes to |
| - | - | - |
| `action` that looks something up (enrichment, waterfall, AI, Claygent) | research | a field in the task |
| `basic` with `settings.formulaText` | logic | code after the task |
| `action` that sends data (CRM, HTTP API, webhook, sequencer) | routing | unchanged, user's code |
| `source` | input | the exported rows; if it has a `search`, also see [searches](https://www.autumn.ai/docs/clay-migration/searches.md) |

Write `clay-to-autumn.json`. Describe what each research value means, not which provider
found it:

```json wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "clay_table": "t_abc123",
  "key": "Record ID",
  "inputs": {"Record ID": "record_id", "Company Name": "company_name", "Domain": "domain"},
  "research": {
    "Employee Count": {"field": "employee_count", "type": "int",
      "description": "Current headcount estimate", "tolerance": 0.25},
    "Sells To Developers": {"field": "sells_to_developers", "type": "str",
      "description": "yes, no or unknown. Does the company sell to developers?"},
    "Domain (corrected)": {"field": "resolved_domain", "type": "domain",
      "description": "The company's real domain, if the input domain is wrong or empty"}
  },
  "logic": {"Tier": "Enterprise if employee_count > 500, else Mid-market"},
  "routing": {"HubSpot sync": "unchanged"},
  "rules": {}
}
```

* `key` must uniquely identify a row in the user's systems. If there is none, ask.
* Don't put the key in `research`. It comes back on every row as an input column.
* Other input columns can come back corrected or not at all, so research fields must not
  reuse input names. Ask for a corrected value as its own field (`resolved_domain`).
* `research` can include fields Clay didn't have. `logic`, `routing` and `rules` can be
  empty (`{}`). Put a per-row check that matters downstream in `rules`, for example
  `{"b2b": "Sells mainly to businesses."}`. Name rules in snake\_case: Autumn returns each
  one as `rule_<name>` and step 5 looks for that exact field.
* A run condition on a research column (`conditionalRunFormulaText`) goes in that field's
  description, for example "only when domain is set".
* `tolerance` is how far apart two numbers can be and still agree in step 5.

Show the user the mapping as a table and wait for a yes.

## 4. Run a sample

```python build_task.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json, re, sys

mapping = json.load(open("clay-to-autumn.json"))
start, size = map(int, sys.argv[1:3]) if len(sys.argv) > 2 else (0, 25)

columns = json.load(open("clay-export/columns.json"))["data"]
names = {c["id"]: c["name"] for c in columns}
inputs = mapping["inputs"]
key = inputs[mapping["key"]]
fields = {c["field"]: c for c in mapping["research"].values()}
if set(fields) & set(inputs.values()):
    sys.exit("research fields must not reuse input names")
for name in mapping["rules"]:
    if not re.fullmatch(r"[a-z][a-z0-9_]*", name) or name.startswith("rule_"):
        sys.exit(f"rename rule {name!r} to snake_case without a rule_ prefix")

rows = []
for line in open("clay-export/rows.jsonl"):
    cells = json.loads(line)["cells"]
    row = {names[k]: v.get("value") for k, v in cells.items()
           if k in names and v.get("status") == "success"}
    rows.append({f: "" if row.get(col) is None else row[col]
                 for col, f in inputs.items()})
rows = rows[start:start + size]

task = {
    "name": f"Clay migration: {mapping['clay_table']} from row {start}",
    "prompt": (
        "Enrich every input row. Return exactly one output row per input row, "
        f"keeping each row's {key}. Fill each field only from evidence; "
        "leave it empty rather than guessing."
    ),
    "input_schema": {f: "str" for f in inputs.values()},
    "input_rows": rows,
    "output": {
        "id": "clay-migration",
        "kind": "research",
        "path": "outputs/clay-migration.jsonl",
        "schema": {f: {"type": c["type"], "description": c["description"]}
                   for f, c in fields.items()},
        "schema_order": list(fields),
        "target_count": len(rows),
    },
    "rules": mapping["rules"],
    "clarify": False,
}
json.dump(task, open("task.json", "w"), indent=2)
print(f"task.json: {len(rows)} rows")
```

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
python3 build_task.py 0 25
TASK_ID=$(curl -sS -X POST "https://api.autumn.ai/task" \
  -H "X-API-Key: $AUTUMN_API_KEY" -H "Content-Type: application/json" \
  -d @task.json | jq -er '.task_id') || echo "start failed"
while autumn wait "$TASK_ID" --json > wait.json; [ $? -eq 3 ]; do :; done
cat wait.json
```

`autumn wait` returns after about 100 seconds with exit code `3` while the task is still
working, so the loop waits again until the task finishes.

* Include a few messy rows (empty domain, name that doesn't match its domain).
* `autumn wait --json` reports `credits_used`. Scale it by `rowCount` in `table.json` and
  tell the user before the full run.
* Rules come back as `rule_<name>` fields (`match` or `unclear`), and Autumn can add rule
  fields of its own. Hold any row where a rule you asked for is missing or any `rule_`
  field isn't `match`.
* Check the output field names against the mapping (`autumn rows "$TASK_ID" --format json`).
  Autumn can normalize a field name (in one test `full_name` came back as `name`). In step 5
  that shows as "Autumn filled 0/N". Map the name back in code; don't ask the task to rename
  it.

## 5. Compare with Clay

```python compare.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import csv, json, os, sys, requests

AUTUMN = "https://api.autumn.ai"
HEADERS = {"X-API-Key": os.environ["AUTUMN_API_KEY"]}
task_id = sys.argv[1]
mapping = json.load(open("clay-to-autumn.json"))
key = mapping["inputs"][mapping["key"]]

def autumn_rows():
    params = {"limit": 500}
    while True:
        page = requests.get(f"{AUTUMN}/task/{task_id}/output",
                            headers=HEADERS, params=params).json()
        for row in page["rows"]:
            yield {k: v.get("value") for k, v in row.items()
                   if not k.startswith("_") and isinstance(v, dict)}
        if not page.get("_next_cursor"):
            return
        params["cursor"] = page["_next_cursor"]

def clay_rows():
    columns = json.load(open("clay-export/columns.json"))["data"]
    names = {c["id"]: c["name"] for c in columns}
    for line in open("clay-export/rows.jsonl"):
        cells = json.loads(line)["cells"]
        yield {names[k]: v.get("value") for k, v in cells.items()
               if k in names and v.get("status") == "success"}

def text(value):
    return "" if value is None else str(value).strip().lower()

def rid_of(row):
    return "" if row.get(key) is None else str(row[key])

def same(a, b, tolerance):
    try:
        return abs(float(a) - float(b)) <= tolerance * max(abs(float(a)), 1)
    except ValueError:
        return a == b

sent = {str(r[key]) for r in json.load(open("task.json"))["input_rows"]}
required = {f"rule_{name}" for name in mapping["rules"]}
rows = list(autumn_rows())
counts = {}
for row in rows:
    counts[rid_of(row)] = counts.get(rid_of(row), 0) + 1

autumn, held = {}, []
for row in rows:
    rid = rid_of(row)
    problems = [k for k, v in row.items() if k.startswith("rule_") and v != "match"]
    problems += [f"{k} missing" for k in required - set(row)]
    if rid not in sent:
        problems.append("unexpected key")
    elif counts[rid] > 1:
        problems.append("duplicate key")
    if problems:
        held.append({**row, "problems": problems})
    else:
        autumn[rid] = row
clay = {str(r.get(mapping["key"])): r for r in clay_rows()}

diffs = []
for clay_col, spec in mapping["research"].items():
    f, tol = spec["field"], spec.get("tolerance", 0)
    n_clay = n_autumn = both = agree = 0
    for rid, row in autumn.items():
        c = text(clay.get(rid, {}).get(clay_col))
        a = text(row.get(f))
        n_clay += bool(c)
        n_autumn += bool(a)
        if c and a:
            both += 1
            if same(c, a, tol):
                agree += 1
            else:
                diffs.append({key: rid, "column": f, "clay": c, "autumn": a})
    print(f"{f}: Clay filled {n_clay}/{len(autumn)}, "
          f"Autumn filled {n_autumn}/{len(autumn)}, agree {agree}/{both}")
returned = {rid_of(r) for r in rows}
print(f"held for review: {len(held)}, "
      f"sent but not returned: {len(sent - returned)}")

out = f"results/{task_id}"
os.makedirs(out, exist_ok=True)
with open(f"{out}/disagreements.csv", "w", newline="") as fh:
    writer = csv.DictWriter(fh, fieldnames=[key, "column", "clay", "autumn"])
    writer.writeheader()
    writer.writerows(diffs)
json.dump(list(autumn.values()), open(f"{out}/autumn_rows.json", "w"), indent=2)
json.dump(held, open(f"{out}/held_rows.json", "w"), indent=2)
print(f"saved to {out}/")
```

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
python3 compare.py "$TASK_ID"
```

Output from a five-row test table:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
employee_count: Clay filled 4/5, Autumn filled 5/5, agree 3/4
sells_to_developers: Clay filled 4/5, Autumn filled 5/5, agree 4/4
resolved_domain: Clay filled 0/5, Autumn filled 5/5, agree 0/0
held for review: 0, sent but not returned: 0
saved to results/e50cf4db186e493b/
```

Each run saves its files under `results/<task_id>/`, so batches don't overwrite each other.
Rows that were sent but not returned go into the next batch again. `held_rows.json` lists
each held row with its problems: a rule that isn't `match` or is missing, a key that wasn't
sent, or a key that came back twice. Only `autumn_rows.json` goes on to the CRM.

The one disagreement was a row whose input domain belonged to a different company: Clay had
enriched the wrong one, and `resolved_domain` caught it. For each disagreement, ask which
value is current. Every Autumn value has a source: open `app_url` from step 4 to see the page
behind each one.

Show the user the summary and `disagreements.csv`, and wait for a yes.

**Read rows as JSON.** The CSV export prefixes values starting with `+`, `-`, `=` or `@`
with an apostrophe, which changes phone numbers. See
[Outputs and sources](https://www.autumn.ai/docs/concepts/outputs.md#download-a-page-as-csv).

## 6. Full run

Run the rest in batches with the same mapping, for example 100 rows at a time:

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
python3 build_task.py 25 100   # then 125 100, 225 100, ...
# start the task and wait as in step 4, then:
python3 compare.py "$TASK_ID"
```

Run each batch through steps 4 and 5 before building the next one, because `compare.py`
reads `task.json` to know which rows were sent. Keep each batch on `input_rows`.
[Attach a CSV](https://www.autumn.ai/docs/guides/file-uploads.md) starts from a draft task, and `output` and `rules` only
apply when a task is created.

When every batch is done, combine the approved rows:

```bash wrap theme={"theme":{"light":"github-light","dark":"github-dark"}}
jq -s add results/*/autumn_rows.json > autumn_rows.json
```

Then run the `logic` columns in code and rename fields back to their Clay column names.

## 7. Cut over

1. Run Clay and Autumn side by side for one cycle. Only Clay writes to production.
2. Point the user's routing code at the Autumn rows, same CRM fields, same names.
3. When the user says so, pause the table's schedule and signals in Clay. Keep the table.

> **[Filter searches](https://www.autumn.ai/docs/clay-migration/searches.md)**
>
> When the table's rows come from Find Companies or Find People.

> **[Signals](https://www.autumn.ai/docs/clay-migration/signals.md)**
>
> Move signals onto your own schedule.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.