Skip to main content
A migration changes the state data of traces that already exist. You declare it once in the configuration project. An administrator then runs it, and the platform writes one signed link per trace — so every change is attributable, visible in the trace history and impossible to confuse with a manual database edit.
A migration reuses everything you already know about effects. What differs is where you declare it: its own migrations map, not actions.

What you write, and what you don’t

This is the whole contract on the configuration side: You never write an initActions entry, a group grant, a transitions entry, hidden: true, or a “mark as applied” call. If you find yourself adding any of those, something is wrong.

1. Declare the migration

string
required
Human-readable label, shown in the run history and in the trace timeline.
ArchiveCondition<TState>
required
Which traces the migration applies to. Non-empty by type: an empty filter would target every trace in the workflow, so it does not compile.
IEffectDef[]
required
The data transformation, exactly as in a normal action.
string
Icon shown alongside the migration.
string
Longer explanation of what the migration does.
string
Defaults to the key you register the migration under.

Writing the filter

filters is the same shape as a workflow’s archiveCondition, so field is typed against your state: a misspelled field name is a compile error.
Rules are combined with AND. Available operators by type:
The filter is what bounds the blast radius. Do not widen it and sort things out inside the effect — write the narrowest filter that selects the rows you mean.There is no regex operator. If the shape you need cannot be expressed, prefer contains on a distinguishing character (ISO dates never contain /, legacy DD/MM/YYYY always does) over selecting everything.
Traces where the field is absent are never selected, so you do not have to guard against missing data in the filter.

Writing the effect

The effect body contains only the transformation. No bookkeeping.
Always return early when there is nothing to change. The filter and the effect have to agree on what “already correct” means, and only the effect can be sure.
This is what keeps a migration safe when the filter selects more than you expected — and a filter eventually will, because it cannot express everything the effect can check. Without the guard, a trace the filter caught by accident is silently rewritten and stamped as migrated, and there is nothing to tell you it happened.A no-op effect still writes a link and still stamps the trace. That is intended: it records that the migration considered this trace and found nothing to do.
If assigning to the field is a compile error, the field is read-only because it reaches your state type only through initialStateData, which is declared as const. typeof initialStateData then types it as a readonly tuple of literals, which no effect can assign to:
Migrate a field your state type declares explicitly instead, or widen the field in the state type first. Do not reach for a cast — the error is telling you the type says this data never changes, which is worth fixing rather than silencing.

2. Register it in migrations

In the workflow’s config, beside actionsnot inside it:
pastaLaVista/index.ts
The key you use here is the migration’s identity. It is what an administrator types to run it, and the key under which each migrated trace is stamped — so treat it as permanent.
Keeping migrations out of actions is deliberate. It means a grant built by mapping over every action (initActions: { kitchenTeam: allActions }) cannot pick a migration up. Config assembly strips them from initActions as well, so you have nothing to remember either way: a migration is never offered to a user because it is never in anyone’s next actions.

3. Declare the traceBot team

Every migration link is authored by the group labelled traceBot. Declare it like any other team, in the same organization as your workflow:
account/teams.ts
Two things here are not optional.The map key must be exactly traceBot. Group labels are generated from the keys of this map, and the platform acts as that exact label.includeSuperUser: true is required. It is what puts the administrator running the migration into the group. Without it, every link is rejected with “the authenticated account is not part of the link group”.
The group is only created when the workflow’s configuration is deployed, so deploy the config before running a migration. Re-deploying is safe: an existing group is reused, never duplicated.
Get either of these wrong and you will not get a broken job — the platform refuses the migration up front with a message naming the fix.

4. Deploy, then hand over

Deploy the configuration as usual. From then on it is an administrator’s job to run the migration; nothing further is needed from the configuration project. They will always dry-run first, which reports how many traces match and changes nothing.

Re-running a migration

When a migration is applied to a trace, the platform stamps it — in its own field on the trace’s state, beside data, never inside it:
Selection skips any trace already carrying the key, which gives you three things for free:
  • running the same migration twice is a no-op
  • if some traces fail, re-running retries only those
  • each migration is stamped under its own key, so migrations never hide each other
You do not write this stamp, and you cannot forget it. It is read-only: the platform recomputes it on every link, so assigning to it from an effect does nothing. Keeping it out of data is what makes it survive an action that replaces data wholesale. Should you ever need to branch on it, it is typed on the state:

One migration, one file, one key

A migration that has run is finished. Never edit it. Once a migration has run anywhere, treat its file as closed. Need a different transformation, a corrected filter, or a second pass over the same field? Add a new migration: a new file, a new key, a new entry in migrations.
This is a rule, not a preference, and there are two reasons for it. The key is the stamp. Traces already migrated carry that exact key, and that is the only thing stopping them being processed again. Rename the key and every one of those traces is selected on the next run. Keep the key but change the effect and the traces that already ran will never get the new behaviour — the two failure modes are opposites, and neither announces itself. The links are the audit trail. Each run leaves one signed link per trace saying what was applied. Editing a migration in place makes that record a lie: two traces show the same action key having done two different things, and nothing distinguishes them.
Keeping old migration files costs nothing. A migration that has already run selects no traces, so leaving it in migrations is free — and it is the record of what was done to the data.

How a migration appears to users

  • Business users never see it. It is not in anyone’s next actions, so it is never offered, and its links are hidden from the trace history.
  • Administrators can reveal the links with a toggle in the trace header, where they are marked distinctly so they cannot be mistaken for business events.
  • The run itself is visible in the workflow’s batch action history, to administrators only, with progress and any failures.

Checklist

Before asking for a migration to be run:
  • it is a new file under a new key — no migration that has already run was edited
  • filters selects only the traces you mean — check the count with a dry run
  • the effect returns early when there is nothing to change
  • the migration is registered in config.migrations
  • a traceBot team exists, keyed traceBot, with includeSuperUser: true
  • the configuration has been deployed

Effects

The business logic a migration reuses

Action definition

Everything else an action can declare

State

The data a migration changes

Accounts

Teams, users and organizations