Skip to content

Add stable flow aliases through DeployedFlow startup deployment #648

Description

@jumski

Summary

Add stable logical aliases for immutable concrete flow versions, and represent deployment metadata with a DeployedFlow wrapper.

Callers start a stable alias such as greetUser. Startup deployment resolves that alias to one concrete version such as greetUserV2. Runs, task state, broadcasts, and history store the resolved concrete flow_slug.

alias is deployment metadata, not DAG behavior. It therefore belongs on DeployedFlow, not Flow.

const GreetUserV2Flow = new Flow<Input>({
  slug: 'greetUserV2',
  // steps
})

export const GreetUserV2 = defineDeployedFlow(GreetUserV2Flow, {
  alias: 'greetUser',
})

A plain Flow remains supported and resolves to a default deployment whose alias and queue are both flow.slug.

This issue does not add named queue routing. The wrapper is the ownership boundary that a later routing issue will extend.

Motivation

Changing flow topology correctly requires a new concrete slug. Changing every application call, trigger, cron job, and SQL function to that slug is repetitive and error-prone.

The design separates:

flow_slug
  immutable concrete version and persisted runtime identity

flow_alias
  stable dispatch name with one active concrete target

DeployedFlow
  code declaration that connects a Flow to deployment metadata

Prerequisite

The public compileFlow() helper may remain, but it must accept the same Flow | DeployedFlow contract or be deprecated. It must not become a second deployment model.

Public API

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

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

EdgeWorker.start(deployed)

Backward compatibility:

EdgeWorker.start(flow)

is equivalent to a default deployment:

alias = flow.slug
default queue = flow.slug

defineDeployedFlow() must preserve the exact Flow type. It must not add another generic parameter to Flow or weaken handler, dependency, condition, input, or output inference.

Data model

Add immutable alias membership and a separate 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 exactly one immutable alias membership.
  • Omitted aliases resolve to the concrete slug.
  • Multiple concrete flows may share one alias.
  • Exactly one concrete flow is active for each alias.
  • An existing concrete slug can never move to another alias.
  • Existing installations backfill every flow as a self-alias.
  • The upgrade must not infer version families from slug suffixes.

Startup deployment and activation

Worker startup deploys or verifies the complete DeployedFlow before worker registration.

Use one lock order everywhere:

alias-scoped advisory lock
concrete-slug advisory lock

Within one transaction:

  1. Register or verify immutable alias membership.
  2. Compile or verify the complete concrete flow definition.
  3. Activate the alias only when this is a brand-new concrete slug.
  4. Commit before worker registration starts.

Activation rules:

  • Compiling a brand-new concrete slug activates it.
  • Rechecking the active concrete slug preserves activation.
  • Rechecking an inactive concrete slug keeps it inactive.
  • Restarting an old worker never reclaims the alias.
  • Local same-slug destructive recompilation preserves prior activation state.
  • Alias membership mismatch always fails, including local development.
  • If worker startup fails after activation commits, keep the alias active. Durable tasks wait for worker recovery.

Two brand-new versions for one alias may compile concurrently. The final alias-lock holder wins. Document that one rollout must deploy only one new concrete version per alias.

Startup results and logs must distinguish compilation from activation. At minimum, expose:

compilation: compiled | verified | recompiled
activation: activated | active | inactive
alias
active concrete slug

An inactive version may still need a worker to drain existing tasks, so inactivity must not fail startup.

Start APIs

Move the current concrete implementation behind start_flow_by_slug() and make alias resolution 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 rules:

  • Keep the existing flow_slug RPC argument name on start_flow() and start_flow_with_states().
  • Backfilled self-aliases keep existing calls working.
  • Missing aliases fail clearly and never fall back to concrete-slug lookup.
  • Alias resolution and concrete start occur in one database call.
  • Runs and all emitted events use the resolved concrete slug.

Client behavior:

  • PgflowClient.startFlow() and PgflowSqlClient.startFlow() use alias semantics.
  • Add startFlowBySlug() for pinned starts.
  • Do not add startFlowByAlias() because alias dispatch is already the default.
  • FlowRun.applySnapshot() must replace its initially requested alias with the resolved run.flow_slug.

Rollback and deletion

Add:

pgflow.activate_flow_version(flow_alias text, flow_slug text)

It must validate immutable membership, take the alias lock, and atomically switch the pointer. It must not require a healthy worker; work may queue until workers recover.

Keep delete_flow_and_data() concrete-slug based:

select pgflow.delete_flow_and_data(
  'greetUserV2',
  drop_alias => true
);

Deletion rules:

  • Deleting an inactive concrete version is allowed.
  • Deleting the active target is blocked by default.
  • drop_alias => true succeeds only when the supplied concrete slug is the alias's sole version.
  • If other versions exist, activate another version first.
  • Same-slug local recompilation must use an internal path that preserves alias membership and activation.

Future queue-routing contract

Named queue routing will extend DeployedFlow, not Flow:

defineDeployedFlow(flow, {
  alias: 'greetUser',
  routing: {
    steps: {
      fetch: 'io_tasks',
      summarize: 'llm_tasks',
    },
  },
})

Routing will be immutable for one concrete slug. A route change creates a new concrete version and activates it through the alias. Existing tasks retain their original queue snapshots.

This changes the current ownership statement:

Runs, task state, and history remain bound to concrete slugs.
Tasks retain concrete slugs and queue snapshots.
Queues may serve multiple concrete flows.
Workers may poll one queue and dispatch several concrete flows.

Compatibility ownership

All versions behind one alias must accept compatible flow input. The user owns this invariant.

Do not add input schemas, schema hashes, runtime compatibility checks, or activation history in this issue.

Acceptance criteria

  • defineDeployedFlow(flow, { alias }) creates a typed deployment declaration without changing Flow generics.
  • EdgeWorker.start(flow) retains current self-alias and flow-slug queue behavior.
  • Every stored concrete flow has immutable, non-null alias membership.
  • The upgrade backfills existing flows as self-aliases without suffix inference.
  • Exactly one active concrete target exists per alias.
  • Startup compiles and activates a new concrete slug in one transaction.
  • Existing active and inactive versions preserve activation on restart.
  • Alias mismatch fails in every environment.
  • Alias and concrete locks use one documented order.
  • Default start APIs resolve aliases, while explicit slug APIs pin versions.
  • Returned clients, snapshots, broadcasts, runs, and history expose the resolved concrete slug.
  • Rollback activation and guarded deletion follow the rules above.
  • compilation: false and its documentation are removed.
  • Startup logs show compilation and activation independently.
  • Tests cover upgrade backfill, concurrent startup, old-worker restart, rollback, deletion, missing aliases, and client snapshots.

Out of scope

  • Named queue routing and queue-centric workers.
  • Mutable routing for an existing concrete slug.
  • Manual steps or external completion.
  • Runtime input-schema enforcement.
  • Alias activation history.
  • Cross-worker readiness coordination or staged activation.

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