Skip to main content
The Migration API has a small vocabulary. Once these few ideas click, the endpoints are obvious.

Source — the system you migrate from

A source is one system you migrate customers off of (Salesforce, a legacy CRM, a pile of spreadsheets). You set it up once, in the Vern UI. Over the API a source is addressed by its name — Vern never exposes a source ID. You can list your sources to let a customer pick which system they’re migrating from, then pass that name as source when you create a migration. A source is optional. A migration that doesn’t name one is “sourceless” — the agent works purely from the uploaded files. A source is not a frozen import plan, either: there’s nothing to hand-author and publish. The import logic is produced by the managed agent at run time, and reused across customers as an optimization.

Template — the object

A template is the target schema for one importable object — a sheet like Contacts or Companies — with its columns and validation rules (which columns exist, which are required, which must be unique, how they link to other objects). Templates are scoped to your account, not to a single source: the same set of objects is available to every source you migrate from. Over the API a template is addressed by its slug — a stable snake_case identifier derived from its name (e.g. contacts). When you create a migration, each template you select becomes one sheet on its workbook. You can list your templates over the API — columns and all — to show a customer what they can import and let them choose.

Migration — the instance

A migration is one customer’s migration — their isolated workspace. You create it with the first API call and it holds a workbook: their data, with one sheet per template. The id returned at creation is the migration ID you pass to every later call. A migration is created against a source (or none), but nothing is bound to it — there’s no frozen plan to replay. Your run and export calls only ever reference the migration; you never pass plan or recipe IDs. The agent decides what logic to run each time. A migration moves through a small set of overall states, derived from what’s happened so far:
  • awaiting_files — created, with no data source yet: no files uploaded and no live source connected.
  • ready — a data source is in place (files uploaded or a source connected), no preview generated yet.
  • awaiting_approval — a preview has been generated and is waiting for you to execute or refine it.
  • completed — an execute run has imported the data.

Live sources

A migration’s data can come from uploaded files, a live API source, or both. A live source is a connector the agent reads from directly at run time — you attach one with POST /source-connection. Once a connection exists, the migration leaves awaiting_files even with nothing uploaded, and a generate run can extract from it (see extract_via_api). If the connector needs secrets, the run pauses for credentials.

The managed agent

Every run executes on one durable, tool-using agent — the same agent that powers Vern’s interactive import UI, driven headlessly. Across a migration’s runs the agent:
  1. Builds. On a generate run it reuses-or-authors the import logic — a portable mapping + transform recipe — and verifies it in a sandbox before touching real data, stopping at a preview.
  2. Self-heals. When a customer’s file drifts from what the recipe expected, it edits and re-verifies until it passes.
  3. Asks, when it has to. If a mapping is genuinely ambiguous and neither the data nor your templates settle it, the run pauses at blocked with a plain-language question. You answer through the run’s messages sub-resource and it resumes.
  4. Executes. On an execute run it runs the real import, then self-heals invalid cells. Surviving invalid cells are reported, not parked — the run completes and the bad cells show up in the report.
The migration is a managed agent, not a frozen-recipe replay. Robustness comes from the agent’s self-heal and its ability to ask — not from a pinned artifact.

Reuse and pinning

By default the agent reuses the source’s most-recent working recipe as a starting point, adapting only what each customer’s upload changed. Because every successful generate writes a fresh recipe, “latest” naturally drifts as different end-customers import slightly different shapes. If you’d rather isolate that drift, you can pin one known-good run’s recipe for a source. Pinned runs read it as a starting point and don’t promote their adapted recipe back to the shared “latest”, so one customer’s run can’t change the starting point other runs draw from. See Pin a source.

Runs

Anything the agent does is a run. You start a run with POST /migrations/{id}/runs and a kind:
  • generate — author the recipe and produce a preview.
  • update — edit the recipe per a message, producing a new preview.
  • clarify — a read-only question about the current preview (changes nothing).
  • execute — run the approved preview for real.
One run at a time per migration — starting a second while one is in flight returns 409. A generate opens a preview run and update/clarify/execute resume that same run, so a single run_id stays pollable from preview through the real import. Each run you poll until it reaches a terminal state:
  • queued — accepted, not started yet.
  • running — the agent is working; the poll response carries a human-readable message.
  • blocked — the agent is waiting on you. blocked_reason says which kind: "question" (an ambiguous decision; the poll carries questions, answered via the run’s messages sub-resource) or "credentials" (a live source needs secrets; the poll carries a credential_request, answered via credentials).
  • awaiting_approval — a generate/update run finished and produced a preview. Fetch it from GET /preview, then refine or execute.
  • completed — finished. An execute run carries a report (rows inserted, per-sheet counts, how many rows still carry an invalid cell). A clarify run completes with no report.
  • failed — the agent couldn’t produce a working result even after self-healing. For an execute, nothing was inserted.
  • canceled — the run was stopped before it finished (see cancel a run).
CSV download is not a run — it’s a synchronous stream of the validated data that already exists in the migration.

Identifiers

  • Migrations and runs are identified by a UUID — store them as opaque strings; don’t assume a typed prefix.
  • Sources are addressed by their name, templates by their slug — there are no public source or template IDs.
  • The external_id you set on a migration is your own string, echoed back untouched, and makes creation idempotent.

Next