Skip to content

Add stable flow aliases through DeployedFlow startup deployment #648

Description

@jumski

Summary

Add stable logical aliases for immutable concrete flow versions through a DeployedFlow wrapper.

Callers start a stable alias such as greetUser. Startup deployment resolves that alias to one concrete version such as greetUserV2. Runs, tasks, queues, broadcasts, and history retain the concrete slug.

Alias metadata remains independent from DAG shape and queue mode.

Dependencies and stage

Public API

Plain flow:

const flow = new Flow<Input>({ slug: 'greetUserV2' })
  .step({ slug: 'greet' }, greet)

export const deployed = defineDeployedFlow(flow, {
  alias: 'greetUser',
})

EdgeWorker.start(deployed)

Private per-step flow:

const routed = withStepQueues(flow)

export const deployed = defineDeployedFlow(routed, {
  alias: 'greetUser',
})

EdgeWorker.start(deployed, { stepSlug: 'greet' })

A plain Flow remains supported and defaults to a self-alias:

alias = concrete flow_slug

defineDeployedFlow() preserves the exact wrapped plain or step-queued flow type. It must not weaken handler, dependency, condition, queue-mode, step-selector, context, or environment inference.

Ownership model

flow_slug
  immutable concrete version and runtime identity

flow_alias
  stable dispatch name with one active concrete target

Flow or StepQueuedFlow
  DAG, handlers, and queue mode

DeployedFlow
  immutable alias membership for one concrete definition

Aliases never appear in generated private queue names. Queue names use the concrete slug so versions remain isolated.

Data model

Add immutable alias membership and one active pointer:

pgflow.flows
  flow_slug primary key
  flow_alias not null
  unique (flow_slug, flow_alias)

pgflow.flow_aliases
  flow_alias primary key
  flow_slug not null
  created_at timestamptz not null
  updated_at timestamptz not null
  foreign key (flow_slug, flow_alias)
    references pgflow.flows (flow_slug, flow_alias)

Invariants:

  • every concrete flow has one immutable alias membership;
  • omitted aliases resolve to the concrete slug;
  • several concrete versions may share one alias;
  • exactly one concrete version is active for each alias;
  • an existing concrete slug cannot move to another alias;
  • existing flows backfill as self-aliases;
  • the migration does not infer version families from slug suffixes.

Startup deployment and activation

Every worker carries the complete wrapped definition. Use one lock order everywhere:

alias-scoped advisory lock
concrete-slug advisory lock

Within one startup transaction:

  1. register or verify immutable alias membership;
  2. compile or verify the complete concrete flow shape, queue mode, and route map;
  3. set the active pointer only when the alias has no active version yet;
  4. commit before worker registration.

Compiling a new concrete version for an existing alias never changes the active pointer. This prevents concurrent V2 and V3 deployments from using last-lock-holder-wins activation.

Activation rules:

  • the first concrete member initializes an empty alias;
  • later concrete versions remain inactive after compilation;
  • rechecking the active slug preserves activation;
  • rechecking an inactive slug leaves it inactive;
  • restarting an old worker never reclaims the alias;
  • local destructive recompilation preserves membership and activation state;
  • alias mismatch always fails;
  • worker startup failure cannot switch an existing alias.

Several step workers may race to deploy the same concrete version. The locks make one compile and the others verify.

Start APIs

Move concrete behavior behind explicit slug functions and make aliases the default:

start_flow()                     alias
start_flow_by_alias()            alias, explicit
start_flow_by_slug()             concrete slug
start_flow_with_states()         alias
start_flow_with_states_by_slug() concrete slug

Compatibility:

  • keep the existing flow_slug RPC argument name on default start functions;
  • backfilled self-aliases keep existing calls working;
  • missing aliases fail and never fall back to concrete lookup;
  • alias resolution and concrete start happen in one database call;
  • returned runs and events expose the resolved concrete slug.

Clients use alias semantics by default and add startFlowBySlug() for pinned starts.

Activation, rollback, and deletion

Add one compare-and-swap operation:

pgflow.activate_flow_version(
  flow_alias text,
  flow_slug text,
  expected_current_flow_slug text
)

It validates membership, takes the alias lock, compares the current pointer, and atomically switches only when the expected current version still matches. The caller uses null only to initialize an alias with no active version.

Production rollout follows #654: deploy and enable the complete new worker set, verify every target queue has coverage, then call activate_flow_version(). Rollback uses the same expected-current check.

Keep deletion concrete-slug based. Rules:

  • deleting an inactive version is allowed;
  • deleting the active target is blocked by default;
  • removing an alias requires that concrete slug to be its sole version;
  • private queues owned by the deleted concrete version follow Persist physical queue identity and ownership for flow tasks #650 deletion rules;
  • future explicit shared queues are never dropped with one version.

Recompilation and membership

Alias membership and the active pointer must survive fenced local recompilation.

Once aliases exist, destructive recompilation replaces only the concrete definition's runtime rows, steps, route metadata, and private queues. It must not delete and recreate the identity row referenced by alias membership.

Take alias and concrete locks in the documented order for activation, recompilation, and alias-aware deletion.

Versioned private queues

For a step-queued deployment:

greetUserV1 -> V1 private step queues
greetUserV2 -> V2 private step queues
alias greetUser -> active concrete version for new runs

Existing V1 tasks never move to V2 queues. Keep V1 workers until no V1 run can create or execute more tasks.

Observability

Startup results and logs distinguish:

compilation: compiled | verified | recompiled
activation: activated | active | inactive
alias
active concrete slug
queue mode
selected worker queue

Do not log credentials or message bodies.

Acceptance criteria

  • defineDeployedFlow() preserves exact plain and step-queued flow types.
  • Plain flows retain self-alias behavior.
  • Every concrete flow has immutable, non-null alias membership.
  • Existing definitions backfill as self-aliases without suffix inference.
  • Exactly one active concrete target exists per alias.
  • New concrete definitions compile completely but do not replace an existing alias automatically.
  • The first concrete member initializes only an empty alias.
  • activate_flow_version() uses expected-current compare-and-swap semantics.
  • Concurrent workers for one concrete version compile once and then verify.
  • Concurrent V2 and V3 deployments cannot produce last-lock-holder-wins activation.
  • Active and inactive versions preserve activation on restart and local recompilation.
  • Alias membership survives replacement of runtime rows, steps, routes, and private queues.
  • Old-worker restart cannot reclaim an alias.
  • Default start APIs resolve aliases; explicit APIs pin concrete slugs.
  • Runs, tasks, private queues, broadcasts, and history retain concrete slugs.
  • Rollout verifies new queue coverage before compare-and-swap activation.
  • Rollback and concrete deletion follow the stated rules.
  • Startup logs separate compilation, activation, queue mode, route map, and worker subscription.
  • Tests cover backfill, concurrent step-worker startup, V2/V3 races, compare-and-swap conflicts, rollback, old-worker restart, local recompilation, deletion, missing aliases, and client snapshots.

Out of scope

  • Explicit custom queue names.
  • Queues shared across flows or versions.
  • Multi-flow worker registries.
  • Mutable routing.
  • Cross-worker readiness coordination.
  • Runtime input-schema enforcement.
  • Alias activation history.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions