Manage workflow storage
Workflow and agent definitions live in a definition store, backed by PostgreSQL — in every environment, development included. Edits made in the console survive redeployments, are shared across proxy replicas, and are versioned: immutable published versions plus an active pointer per definition, a single mutable draft, optimistic concurrency, instant rollback.
Git does not lose its place: it stays the language (YAML), the seed and the archive — see the two sections below.
Point it at a database
Set DEFINITION_STORE_URL on the endpoint and proxy (postgres://…). It may be the
same database as the run index (RUN_DB_URL) — the tables are disjoint — and that is what
happens by default: with DEFINITION_STORE_URL unset, a Postgres RUN_DB_URL is used.
With neither, boot fails with an explicit message rather than starting half-configured.
Locally, docker compose up -d postgres provides it; the compose file publishes it on
host port 5433 (not 5432) so it cannot collide with another Postgres already running
on the machine — override with POSTGRES_PORT.
There used to be a
file:<dir>driver serving the repository's YAML directly. It was removed before any production use: having no versions and no draft, it left half the product behind acapabilities.versionsflag that reached into the editor, and two drivers meant two chances to diverge silently.
The engine itself never reads the store at run time: each run's spec is journaled in its Restate invocation, so replay is pinned regardless of later edits. Sub-workflows follow the same rule — when a run starts, the proxy resolves every workflow the graph can call (transitively) and pins those specs into the invocation too. Two consequences worth knowing:
- a workflow that exists only in the store (created in the console, never written to
workflows/) is callable as a sub-workflow; - a child's version is frozen at the parent run's start, so publishing a new version of a sub-workflow leaves runs already in flight untouched.
Saving from the console re-grafts the previous version's YAML comments onto the new spec: the editor carries only the structure, and those comments carry the why — without this, the first console save would erase what the seed brought over from git.
A workflow created in the console starts as a draft: its first save calls
PUT /api/workflows/<slug>/draft with create: true, which is refused with a 409
(code: "exists") when the slug already exists in any state, archived included, or
belongs to an agent: workflows and agents share one slug space. The check runs in the
write transaction, so two simultaneous creations of one slug do not overwrite each
other. A seed root holding a workflow and an agent with the same slug is refused at
boot. When the
workflow comes from an imported YAML file, the file's text travels with that first save
(sourceYaml) and its comments are kept in the draft.
Seed a fresh store from git
On boot, if the store is empty, the proxy imports every definition from the seed
roots as published v1: the host's system definitions
(DEFINITION_SEED_DIR, default apps/api — only agent-invoke.workflow.yaml
lives there), plus every directory listed in the instance manifest's seed:
field — this is how shipped content stays instance data rather than host code
(the clinical demos live in content/clinical-demo, seeded by
seed: [content/clinical-demo]; the Docker image embarks content/).
Once the store holds definitions, a definition it knows is never overwritten, even
when its seed file changed. A seed file whose slug the store does not know (archived
and unpublished slugs count as known) is a new file shipped by an upgrade: it is
seeded at boot, published. When its event already starts a published workflow, it is
seeded as a draft instead, with the log line
Graine « <slug> » semée en brouillon : l'événement <type> démarre déjà « <slug> ».,
so that it does not start on that event before someone reviews it and publishes it.
Seed without publishing: drafts/
Files under workflows/drafts/ or agents/drafts/ are seeded as drafts — never
published, so they never receive events. Use it for bench fixtures and work-in-progress specs that
must ship with the seed without claiming an event type. The slug comes from the file's
base name, not its path, so moving a file into drafts/ keeps its identity — and a file
with the same base name at the root and in drafts/ is refused at boot. The boot log
reports the two populations separately (published vs. drafts).
Editing YAML by hand: DEFINITION_SEED_ALWAYS
Set it to 1 and the proxy re-publishes the seed directory at every boot, so the
development loop stays what it always was: edit a YAML, restart, it is live. It is
idempotent by content hash — a file identical to the active version publishes nothing, so
ten restarts do not create ten versions. A drafts/ file follows the same rule against
the current draft (never the active version), and is still never published.
Only ever set this in development. Anywhere definitions are edited in the console, it would overwrite them on every restart.
Export back to git
The store is the live truth; git remains the archive and review channel:
pnpm --filter @meridian/api run definitions:export -- ./definitions-export # active versions → YAML
pnpm --filter @meridian/api run definitions:import -- ./some-dir # publish as NEW versions
Import never overwrites history — each file becomes a new published version.
definitions:import reads <dir>/workflows and <dir>/agents only, not their
sub-directories (so not workflows/drafts/), replaces the current draft of each
imported slug, and publishes without the validation of the console. An imported
workflow starts on the events of its type as soon as it is published, alongside
the other workflows that receive them. A relative <dir> is read from the directory
where you type the command. The target store is the one of DEFINITION_STORE_URL.
Use export/import to back up, review changes in a PR, or promote definitions between
environments.
Roll back a bad publish
Rolling back moves the active pointer to an earlier published version — instant, no
redeploy, and in-flight runs are unaffected (their spec is journaled). Authors do it from
the workflow view (see Publish and roll back),
or via the API: POST /api/workflows/<slug>/rollback {"to": N}.
Multi-replica behavior
Each proxy replica keeps an in-memory routing cache and polls the store's generation counter (≤ every 3 s): a publish on one replica reaches the others within seconds.