# Loop Workflow Platform

> Canonical path: `specs/workflow/agentteam-workflow-platform.md`
> Status: living-spec
> Scope: Workflow object ownership, Plan authoring and versioning, Team-owned reusable Workflow authoring and direct publication, the `loop.plan/v1` graph contract, direct Plan invocation, and the durable boundaries that schedule Binding, Invocation, and Run must implement.
>
> Related specs:
>
> - [`specs/automation/run-effect-contract.md`](../automation/run-effect-contract.md)
> - [`specs/collaboration/collaboration.md`](../collaboration/collaboration.md)
> - [`specs/message`](../message/README.md)

## 1. Product boundary

Loop Workflow is a workspace-scoped orchestration subsystem with four
first-class objects and two explicit paths into execution:

```text
Plan ---------------------------> Invocation -> Run
  `-> Binding (schedule only) --> Invocation -> Run
```

- **Plan** owns reusable authoring logic and its version history.
- **Binding** is an optional durable deployment for automatic schedule
  activation. Every revision pins one exact PlanVersion and owns the schedule
  source, lifecycle, and canonical execution owner.
- **Invocation** records one accepted occurrence. A direct manual occurrence
  starts from the Plan itself; a scheduled occurrence starts from a Binding.
  Both snapshot one exact PlanVersion, validated input, trusted execution
  identity, and source provenance.
- **Run** executes the Invocation snapshot. It references the Invocation, Plan,
  and exact PlanVersion and owns tokens, node activations, attempts, waits,
  effects, state, events, and terminal status. Run has no Binding fields.

`PlanVersion` and `BindingRevision` are immutable supporting snapshots, not
additional first-class product objects. A direct run does not synthesize a
Binding or BindingRevision. Plan authoring and schedule deployment are
intentionally separate: a reusable Plan never contains a trigger, schedule,
execution principal, `from`, or `runAs` value.

每个 Team 最多拥有一个可复用 Workflow。Team 所有权由持久化的 Team-to-Plan
关系表达，该关系拥有独立的编写与审批生命周期；它不会引入第五种运行时对象，也不会
重复存储 Plan source 或 version。已审批版本发布后，Team Workflow 仍通过普通的
Plan、Invocation 和 Run 合同执行。

## 2. Object ownership

### 2.1 Plan aggregate

A Plan is the workspace-scoped identity for one reusable graph. It owns:

- stable Plan ID, workspace ID, and workspace-unique Plan key;
- one explicit canonical top-level `workflow` Scope used only by the Builder's
  Agent-edit transcript;
- display name and description;
- lifecycle status `active` or `archived`;
- one mutable server-side `PlanDraft`;
- zero or more immutable `PlanVersion` records;
- nullable `currentVersionId` and `defaultVersionId` pointers;
- authoring provenance and timestamps.

Plan authoring provenance includes actors such as `createdBy`, draft `savedBy`,
metadata `updatedBy`, version `publishedBy`, and archive/default-promotion
actors. These fields explain who authored or managed the Plan. They do not grant
runtime authority and never determine the author of a Workflow effect.

A Plan is not in a `draft` or `published` lifecycle state. An active Plan may
have no published versions or many published versions. The Plan status
describes availability of the authoring aggregate; draft and version roles are
independent facts.

The Workflow Scope is created atomically with the Plan, initial draft, and
creator membership. Its persisted ID is read through `workflow_plans.scope_id`;
callers must not derive it from the Plan ID. Plan archive and restore are the
only lifecycle authority for this product Scope. Archiving does not create,
archive, replace, or delete the Scope, its memberships, or its transcript.

The upgrade that introduces this required relation runs in the release
activation's exclusive schema cutover after the previous `agentteam-server` has
stopped. It creates and validates canonical Scope and creator-membership rows
for existing Plans before adding the unique, foreign-key, and non-null
contracts. Rewriting the `scopes` type/topology checks may scan existing Scope
rows; Plan relation and membership backfill scales with existing Plans and
does not scan historical messages. The new server and Workflow UI start only
after schema verification succeeds. Rollback disables the Workflow UI or moves
forward with a compatible binary; it never deletes the new Scope transcript or
runs a destructive down migration.

### 2.2 PlanDraft

Every Plan has exactly one mutable PlanDraft stored by the server. The draft
contains:

- `loop.plan/v1` source text;
- visual layout and other non-executable Builder metadata as a sidecar;
- a monotonic draft revision and ETag;
- last-save provenance.

The executable source contains no Plan name, key, owner, archive state, or
visual layout. Plan metadata belongs to the Plan row; layout belongs to the
draft/version authoring sidecar. Neither is part of runtime semantics or the
compiled digest.

Draft save is an optimistic-concurrency operation. A client must submit the
revision or ETag it loaded. A stale save returns a conflict with the current
revision and never overwrites newer server state. The browser may temporarily
hold invalid source while displaying diagnostics, but the server persists only
source and layout that pass the complete parse, validation, and compile
boundary. A no-op save does not create a new draft revision. Validation is an
explicit, non-mutating operation and returns the same structured diagnostics
used by save and publish.

Validation may target the stored draft revision or unsaved source. Validation
does not create a version or move either version pointer. Publish always parses
and compiles the exact requested draft revision again; a cached validation
result is not authority to publish stale content.

Stored-draft validation is Plan-scoped and names the exact draft revision to
load; it never accepts client-echoed source as proof of that revision. A stale
request reports structured `expectedRevision` and `currentRevision` values.
Because validation is read-only, preserved drafts of archived Plans remain
valid inspection targets.

### 2.3 PlanVersion

Publishing appends an immutable PlanVersion. A version contains:

- a stable version ID;
- exact published `loop.plan/v1` source;
- an immutable snapshot of non-executable authoring layout;
- explicit, versioned compiled IR;
- compiler version and semantic digest;
- publish provenance and timestamp.

Source, layout, IR, digest, and provenance are immutable. A version is never
edited in place or individually deleted. Reverting executable behavior uses an
explicit republish command that takes one historical version ID as source and
appends a new PlanVersion. The historical version and current draft remain
untouched. Plan-local display ordinals are not part of the storage, API, or
product contract; history is ordered by publish timestamp with immutable ID as
the deterministic tie-breaker.

Publishing an active Plan is one transaction that:

1. locks and revalidates the expected Plan revision and requested draft
   revision;
2. strictly parses, validates, and compiles that revision;
3. appends the immutable PlanVersion;
4. moves `currentVersionId` to the new version;
5. initializes `defaultVersionId` on first publish, or moves it when this
   publish explicitly requests `makeDefault`;
6. retains the still-editable draft unchanged.

Any compiler error aborts the transaction. No partial version, pointer change,
or compiled artifact may remain.

Historical republish follows the same atomic compile-and-append boundary using
the historical version's immutable source and layout rather than the draft. It
always creates a new version, advances current, optionally promotes that new
version to default, records new publish provenance, and leaves the draft and
source version unchanged.

### 2.4 Current and default versions

Current and default are distinct roles over immutable versions of the same
Plan:

- **current** is the most recently published version. Only successful publish
  advances it. It never points backward.
- **default** is the authoring-level recommended version shown by Plan read and
  Builder surfaces. The first publish initializes it. Later publishes do not
  change it unless that publish explicitly requests `makeDefault`.

Changing the default is either an explicit promotion of an existing version of
the same Plan or the explicit `makeDefault` option on publish/republish.
Promotion is CAS-protected and does not modify the selected version, current
pointer, or draft. A Plan with published versions cannot have an unset default.

Every BindingRevision pins one exact PlanVersion ID. A create/edit UI may use
current or default as an initial authoring suggestion, but the persisted
BindingRevision must contain the resolved immutable ID, never a moving
`current` or `default` selector. Pointer movement therefore cannot change an
existing BindingRevision or scheduled Invocation.

A direct Invocation selects a version from the Plan without creating a
Binding. Omitting `planVersionId` selects the Plan's default version; supplying
it selects that exact immutable version and verifies that it belongs to the
Plan. Acceptance snapshots the resolved PlanVersion, so later default-pointer
movement cannot change an accepted Invocation or its Run.

## 3. Plan lifecycle and server contract

### 3.1 Required operations

The workspace Plan API exposes distinct operations for:

- create, list, and read;
- update Plan display metadata;
- read and CAS-save the server draft;
- validate draft or unsaved source;
- publish an exact draft revision;
- list and read immutable versions;
- republish a historical version as a new immutable version;
- promote a version to default;
- archive and restore the Plan.

Archive and restore are explicit lifecycle commands. Plan v1 has no deletion
operation, and a delete-shaped API must not masquerade as archive.

All reads and writes are workspace-scoped and permission-checked. Mutations
return the resulting Plan/draft revision or ETag so another writer cannot
silently overwrite them. Plan metadata CAS and PlanDraft CAS are independent;
editing a display name does not manufacture a draft conflict.

Direct Human authoring mutations for an ordinary Plan require the authenticated
workspace member to match the Plan's durable authoring member. For a
Human-created Plan this is the immutable creator member. For an Agent-created
Plan this is the Human `owner_member_id` on the canonical Workflow Scope's
active creator membership. The Plan's `createdBy` remains the Agent provenance
and is never rewritten to grant Human authority. Browser Plan detail responses
expose the server-evaluated `canEdit` decision; clients must not infer it from
`createdBy`. Other workspace members retain their existing read and Run access.
Plans linked to Team Workflows use the Team Leader direct-publication and lifecycle authority instead.

Plan and PlanDraft ETags bind both the resource kind and exact Plan ID in
addition to the revision. An ETag loaded for one Plan or for its draft is not a
valid precondition for another Plan or for the other resource kind, even when
their numeric revisions happen to match.

Create, publish, and historical republish require a bounded caller idempotency
key. Create keys are scoped to authenticated actor and workspace. Publish and
republish keys share one scope per authenticated actor, workspace, and Plan.
Retrying the same logical request returns the same created Plan or PlanVersion
identity without creating a duplicate. Reusing that key for a different
payload is a conflict.

### 3.2 Strict requests and resource budgets

Every Plan JSON mutation or validation endpoint accepts exactly one JSON object
whose wire representation is at most 4 MiB. Object keys are unique and must be
declared by that endpoint's request schema; unknown keys, duplicate keys, and a
second trailing JSON value are rejected with `400 plan_invalid_json`. A body
over the wire limit is rejected with `413 plan_request_too_large`. Required
replace fields retain presence separately from their value: in particular,
omitting `source` from a draft replacement is invalid and cannot silently
replace the draft with an empty source.

The following UTF-8/JSON byte and structural limits are part of the Plan v1
authoring contract:

- Plan source: 1 MiB; source YAML nesting: 64; source YAML structural nodes:
  100,000;
- visual layout sidecar: 256 KiB; Plan description: 64 KiB;
- graph: 512 nodes and 2,048 edges;
- declared schema fields, including nested array item schemas: 2,048;
- one `sendMessage.relatedScopes` list: 64 scope expressions;
- one callback completion: 256 WorkItems; one WorkItem key: 256 UTF-8
  bytes; one WorkItem's `relatedScopes`: 64 scopes; one batch: 2,048
  dependency edges, 512 related-scope references, 6,144 canonical JSON values,
  and 1 MiB of canonical WorkItem JSON;
- one `fan_out.maxConcurrency`: 64, with a compiler-frozen default of 4;
- literal values: nesting 32 and 8,192 total values;
- one expression: 8 KiB and nesting 64; one interpolated template: 64 KiB;
- one counted loop's `iterations`: 10,000; one retry policy's
  `maxAttempts`: 100.

Before lowering, compilation uses saturating arithmetic to project every
materialized contract copy, binding/expression instruction, template segment,
runtime port, and validated scope record. It rejects a projection above 16,384
schema fields, 32,768 expression instructions, 131,072 IR units, or 64 MiB of
conservatively estimated materialized/wire data, including worst-case JSON
escaping of every arbitrary string retained in the artifact, with
`resource.artifact_budget`; this projection never enumerates the Cartesian
product of fields and ports or edges. The completed normalized artifact still
has to pass the stricter final gates: canonical JSON is at most 8 MiB with
envelope nesting at most 256 and expression nesting at most 128. These larger
artifact counts account for contracts materialized on multiple runtime ports;
they do not increase the smaller authoring-source limits. Budget diagnostics
are stable compile errors and produce no PlanVersion or pointer movement.

### 3.3 Create, list, read, and update

Creating a Plan atomically creates an active Plan and its initial server draft.
It does not publish a version. Both version pointers are null until the first
successful publish.

List returns active Plans by default and includes archived Plans only through an
explicit filter. Plan read surfaces lifecycle, authoring provenance, and
current/default summaries. The independent draft read surfaces its draft
revision and ETag. Neither read treats mutable draft source as a published
version. Archived Plans and all their versions remain readable.

Metadata update changes only Plan-owned mutable display metadata and update
provenance. The Plan key remains stable. Metadata update cannot change source,
layout, a version, execution owner, current, or default.

The version-history collection returns lightweight immutable metadata summaries
only. It neither selects nor serializes version `source`, visual `layout`, or
compiled `artifact`. An exact-version read returns the complete immutable
PlanVersion, and publish or republish responses likewise return their complete
new version, so authoring clients can fetch large payloads only when needed.
Authoring clients load Plan and Draft for the normal first paint and may load
at most five lightweight summaries for the title's recent-version menu. They
fetch complete source and layout only when the Human selects a version, load
the full history's first bounded page only when history opens, and continue the
summary list through its cursor.

Agent-facing Workflow Exec reads obey the same lossless bounded rule documented in the [daemon continuation contract](../daemon/workflow-exec-payload-reads.md). `workflow
plan show` may return a compact draft view; `workflow plan version` reads exact
immutable version metadata, while `section: source|layout` plus an opaque
cursor retrieves the complete payload in bounded slices. `workflow run show`
uses compact metadata by request and exposes `section: input|output` with
an optional JSON-pointer path; `workflow run logs` keeps event chronology
paginated and reads oversized event details with the stable Run ID, event
sequence, and continuation cursor. `workflow step get` preserves the normal
callback authorization and offers `section: input|output_contract|
work_items_contract` continuation reads. Cursors are bound to workspace and
Plan/Run/Step/version identity plus the authoritative immutable content
revision/digest. Plan draft updates and live payload changes invalidate a
continuation with a stable conflict diagnostic and no payload bytes; clients
restart with an empty cursor. A slice is exact text/JSON and is never a
semantic truncation. Small responses retain their existing convenient fields.

### 3.4 Archive and restore

Archive is the normal reversible removal operation:

- it changes `active` to `archived` under Plan CAS;
- it preserves the draft, versions, current/default pointers, provenance, and
  runtime references;
- it keeps the workspace-unique Plan key reserved;
- it makes Plan authoring, publish, default promotion, and creation of new
  Bindings or direct Invocations from that Plan unavailable until restore;
- it does not cancel Runs or silently disable existing Bindings. Binding owns
  deployment lifecycle and execution authority.

Restore changes an archived Plan back to active under CAS and preserves all
aggregate contents. Repeating archive or restore against the already-requested
state is idempotent when the supplied revision still identifies that state.

Archive is the only removal surface for Plan v1. The archived aggregate,
draft, immutable versions, provenance, and execution references remain durable
and readable; returning it to authoring uses restore.

### 3.5 Team-owned reusable Workflows

每个 Team 可以拥有零个或一个可复用 Workflow。Team Workflow 是一个 Team 与一个
workspace Plan 之间的持久关联。该关联拥有 Team 专属生命周期，并至少包含：

- a stable Team Workflow relation ID, workspace ID, and Team ID;
- the linked Plan ID after the Team Leader creates the Plan;
- the Team Channel, daemon command ID, and authenticated requesting Leader
  Member used for idempotency and audit;
- authoring state, published version identity, latest revision,
  activation time, bounded failure details, and deletion provenance.

The relation ID, not the Plan ID, is used by Team-nested list, detail, and
delete routes. Plan remains the sole owner of mutable draft source and
immutable PlanVersions. The relation never copies Plan source or becomes an
alternative publication store.

Team Workflow authoring and executable availability are independent
dimensions. Authoring state is one of `generating`, `idle`, or `failed`; deletion is represented
by a durable tombstone. Availability is `unpublished`, `active`, or
`archived`, derived from the linked Plan and its approved current version. A published Team Workflow remains `active` while later direct authoring work is in progress; existing Runs remain pinned to their immutable versions.
The Team UI may expose a derived status, but it must retain both dimensions in
the server response rather than collapsing them into one lossy enum.

### 3.6 Team authoring and direct publication

创建入口位于 Team settings 成员列表下方的 **Workflow** 区块。创建操作会把 Human
带回该 Team 的 Channel，并在草稿中通过真实 mention 指向 Team 的固定 Leader
Agent。该操作保留输入框中的既有草稿，且绝不自动发送。

仅当 Team 不存在未删除的 Team Workflow 关系时，当前 Team Leader 才能创建关联
Plan。创建在一个事务内锁定 Team、创建 Plan/draft、编译 source、追加首个 immutable
PlanVersion、设置 current/default，并把 relation 置为 `idle`。编译诊断、Leader
授权或任一 CAS 失败都会回滚全部写入。

后续 Team 更新使用带 Team ID 的直接 `update` 操作。服务端在同一事务内重新验证
当前 Leader，比较 Plan、draft 和 relation revisions，保存精确 draft，编译 artifact，
追加 immutable PlanVersion，设置 current/default，并前进 relation revision。相同
client request 重放返回原版本；不同 payload 复用 request ID 会冲突。直接更新不会
创建 generic Approval、historical proposal、assignment、Approval card 或 Team Leader ownership
Inbox entry。

归档、恢复和删除继续走 Team ownership boundary。删除保留 durable ownership tombstone；
Plan version、actor history 和既有 Run pinning 不被重写。Migration cutover 对历史
pending Team proposals 只做取消/失效，不应用其 frozen payload，并使 relation revision
前进，调用方必须以 cutover 后 revision 更新。

### 3.7 Team Leader execution choice

For each Team request, the Leader chooses the least orchestration necessary:

1. **direct discussion** — answer or coordinate in the Team conversation
   without creating or invoking a Workflow;
2. **published Team Workflow** — 当 Team 的可复用 Plan 存在当前可执行的已发布版本时
   调用该 Plan；
3. **dynamic one-off Workflow** — construct an ad-hoc Workflow for a request
   that does not fit a reusable Plan.

Dynamic one-off Workflow generation is a built-in Team capability. It has no
Team setting, enable switch, Agent picker, or separately associated reusable
Plan that the Human must configure in advance. It does not appear as a Team
Workflow merely because the Leader chose dynamic orchestration for one task.
Creating a reusable Team Workflow is always the explicit authoring and direct-publication
flow in §3.6.

Only published, executable Team Workflows are candidates for the second choice.
Generating, unpublished, failed, deleted, and archived
relations are not executable candidates. A direct update remains a candidate
through its last published version until the new version commits; an unpublished
draft is never executable. The Team runtime prompt and authenticated Agent Plan list
surface must make this distinction available to the Leader without relying on
a Human-authored prompt to teach the policy.

### 3.8 Team Workflow product surface

Team settings 在只读成员列表之后紧接一个 **Workflow** 区块。因为每个 Team 最多
拥有一个当前关系，所以不增加独立的 Team Channel tab、搜索控件或筛选器。若旧数据
中仍存在多条未删除关系，区块必须明确提示用户清理、保留所有关系的管理入口，并按需
加载更多；不得静默隐藏或自动删除旧关系。

当前关系不存在时，该区块展示简洁空态和一个创建按钮。按钮复用 §3.6 的 Team
Workflow 需求对话框与普通 Team Channel 草稿交接。关系存在时，该区块在与成员卡片
相同的响应式网格中展示同宽的 Workflow 卡片。卡片沿用 workspace Workflow 的视觉
语言、生命周期状态、加载、权限和错误反馈。

卡片操作由服务端权威状态派生，包括打开已建立的 Plan、在被要求修改后继续 draft、
重试失败、为待审批请求打开 Approval Inbox，以及删除关系。Action flag 只是 UI
提示，不是授权能力；所有操作仍须通过服务端授权与 revision 校验。从 Team chat
返回时必须重新加载当前关系；设置页保持挂载时还要协调 Team Workflow 生命周期事件。

选择卡片会直接进入其关联 Plan 的既有 workspace Workflow detail/editor。关联和发现
仅从 Team 指向 Workflow：workspace Workflow 列表不是 Team 关联选择器，Workflow
详情也不需要反向 Team 管理界面。Team settings 是创建、检查生命周期、重试或删除
Team 可复用 Workflow 的 canonical 入口。

### 3.9 Deletion and publication-boundary enforcement

Deleting a Team Workflow is a Team-owner-authorized domain operation, not an
independent Plan hard delete. In one transaction it:

1. archives the linked Plan without deleting its draft, immutable versions, or
   runtime references; and
2. tombstones the Team-to-Plan relation with deletion provenance.

The tombstone remains authoritative so the archived Plan cannot later appear as
an unowned workspace Plan. Repeating the same delete request is idempotent; a
stale relation revision cannot delete newer authoring work.

Every Human and Agent workspace-level Plan publication surface must consult the
Team relation before `publish`, historical `republish`, or `make_default`. When
the Plan is Team-linked, those direct operations fail and instruct the caller
to use the Team Leader direct update operation. The Team update service owns
Team authorization, compile diagnostics, Plan/draft/relation CAS, and atomic
version publication. Calling a lower-level or workspace-level Plan API must
never bypass that boundary.

## 4. `loop.plan/v1` source contract

### 4.1 Header and semantic boundary

Executable source starts with exactly:

```yaml
apiVersion: loop.plan/v1
kind: PlanVersion
```

The remaining document declares the Plan input contract, optional typed Run
state, Plan output contract, one explicit graph entry, stable nodes, and stable
edges. The graph is the only source of control flow.

The source must not contain:

- Plan identity, display metadata, authoring provenance, lifecycle, or visual
  layout;
- Binding schedule/trigger configuration, execution owner, `from`, or `runAs`;
- Invocation source occurrence or actor provenance;
- Run IDs, token IDs, callback capabilities, placement tokens, or timestamps.

### 4.2 Type contracts

Plan input, Plan output, Run state, and node input/output use the same bounded
type and constraint surface:

- types: `scope`, `channel`, `string`, `enum`, `number`, `boolean`, `member`,
  `duration`, `datetime`, `object`, `array`, `agent`, and the nominal
  compiler-owned `work_items`;
- constraints and authoring hints: `required`, `default`, `values`, `items`,
  `label`, and `description`.

`scope` is the portable identity of one existing Loop collaboration space. Its
only wire representation is the exact object
`{"kind":"channel|thread|dm|task","scopeId":"..."}`. The object rejects unknown
fields, empty or non-canonical IDs, unknown kinds, the message-target
`reply_thread` alias, and the Team pseudo-target. Kind is the persisted scope
kind and is not inferred from the ID prefix. A scope value is identity, not an
authorization grant: every effect or callback boundary that consumes it must
load the active scope, prove that it belongs to the Run workspace, compare the
persisted kind, and authorize its actors again.

`channel` is the legacy string-shaped compatibility type. It remains readable
by existing published Plans but is neither implicitly converted nor assignable
to `scope`; new Plans and official examples use `scope`.

`work_items` is not an author-defined array schema. It names the closed,
compiler-owned output of a callback that opted into dynamic WorkItems and is
consumed by `fan_out`. Authors carry that value through node inputs and edge
bindings without fabricating, inspecting as a generic array, or redefining its
item schema.

Human-facing invocation and Binding forms are projections of the selected
PlanVersion's exact input contract, not generic JSON editors. Boolean and enum
values use closed selectors; number, duration, and datetime values use their
type-specific controls; arrays recursively render their declared `items`
schema. Workspace-backed `agent`, `member`, `channel`, and `scope` values are
selection-only: the UI may show only access-filtered, cursor-bounded resources
and must not offer an arbitrary custom-value path. A selected `channel`
serializes the legacy `#slug` string, while a selected `scope` serializes the
exact `{kind, scopeId}` object. `work_items` is never rendered as invocation
input. Server-side normalization and authorization remain authoritative.

Starting a Run from Plan detail opens one focused dialog over the exact default
published PlanVersion. The dialog uses the same schema-driven controls and
validation as Binding input, preserves the current draft and field errors after
a failed invocation, and never redirects required input into AI edit. The
dialog may expand and collapse a read-only graph preview; hovering or focusing
an input emphasizes the graph Nodes that directly consume it. The preview
reuses the Builder canvas and provides an explicit `Edit workflow` return
action rather than a second graph implementation.

工作流详情中的“运行”保持为稳定的主操作，始终启动一次即时运行。与其相连的紧凑次级菜单固定提供“添加定时任务”和“管理定时任务”两个入口；使用次级入口不会替换或记忆主按钮的行为。

Human-facing product copy calls a reusable Plan a Workflow and a
schedule-backed Binding a Schedule or the locale's familiar equivalent, such
as 定时任务. Plan, Binding, and trigger remain technical model and API terms.

The workspace Workflow entry opens `/workflow` directly and has no level-2
sidebar. Its collection routes share one stable Workflow module header and one
Router-backed tab row ordered **Overview**, **Workflows**, **Schedules**, and
**Runs**. The routes remain `/workflow`, `/workflow/plans`,
`/workflow/bindings`, and `/workflow/runs`; Workflow, Schedule, and Run detail
routes remain outside that collection layout so existing deep links retain
their focused detail chrome. The module header follows the shared primary
header contract without a decorative icon. Semantic icons remain consistent
across tabs, empty states, actions, and detail surfaces where an icon conveys
meaning.

The Overview's primary region is a switchable three-local-day calendar. Its
initial window contains yesterday, today, and tomorrow, with today in the
middle. Previous and next navigation move by three days and permit both past
and future windows. The displayed date range opens a date picker for choosing
an arbitrary three-day window. 日期导航位于工具栏左侧；右侧范围控件在 workspace
全部 Workflow 与当前成员或其拥有的 Agent 所创建的 Workflow 之间切换。Agent 归属
只在用户显式选择“Mine”后按需解析，不得成为 Overview 首屏的额外数据依赖。

Each day is one column of full-width items ordered chronologically; unused time
gaps do not consume vertical space, and simultaneous items remain separate
sequential cards rather than overlapping or narrowing. Accepted Runs and
future Schedule occurrences use the same calendar hierarchy. Card height uses
bounded duration tiers: short Runs stay compact, medium Runs grow once, and
Runs of at least one hour use the tallest capped size. Medium and long Run cards
show elapsed duration plus the available start/end range; active Runs calculate
elapsed time from the current time. A future occurrence has no Run status or
duration and is presented as scheduled, not queued or running.

The adjacent dashboard summarizes only the selected window and scope. Its
health metrics form one bordered two by two grid, followed by one stacked
horizontal bar for succeeded, failed, and in-progress proportions with a compact
single-row legend. The secondary list
shows the ten most recent Runs in descending start-time order without filtering
by status. On wide panes, only this recent-Run list scrolls, its scrollbar
remains hidden, and the health summary stays fixed. Each row opens that Run
directly. Every item in the selected window remains accessible in the shared
internally scrollable calendar; the calendar does not replace later items with
an overflow count.
The Overview does not fan out into per-Run or per-Schedule reads.

The canonical calendar read is:

```http
GET /api/v1/workspaces/:workspaceId/workflow/calendar?from=<RFC3339>&to=<RFC3339>
```

`from` and `to` are required instants and define a half-open `[from, to)`
window. The browser converts the selected local-day boundaries to exact
instants before calling the API; authored cron timezones remain part of each
Schedule and are not replaced by a request timezone. The API accepts windows
of at most 31 days and returns one complete, ascending `items` collection with
at most 5000 items. If the complete result would exceed that safety budget, it
returns `422 workflow_calendar_window_too_dense` and requires a narrower
window; it never silently truncates or paginates a calendar window.

Each item has `kind`, `occursAt`, and Workflow identity. A `run` item contains
the accepted Run's identity, status, and lifecycle timestamps. Its `occursAt`
is the immutable Invocation `source_occurred_at`, so a delayed automatic Run
stays aligned with the Schedule occurrence that caused it. A `scheduled` item
contains Binding revision/source identity and the stable occurrence key but no
Run. It is projected from the current enabled Binding's active Schedule source
and earliest-unaccepted occurrence cursor; projection reuses the execution
scheduler's cron timezone, interval anchor, and one-shot semantics. Projection
is read-only and must not create an Invocation or Run ahead of execution.

The Run window and active Schedule cursors are read from one consistent
database snapshot. If an occurrence has already been accepted, the `run` item
replaces the projected `scheduled` item with the same occurrence key. Disabled,
archived, superseded, and completed Schedule sources do not produce future
items. This window contract is independent of the cursor-paginated recent Run
collection and must not inherit its page-size limit.

Workflow and Schedule detail use the same shared single-row hierarchy as
Channel and Thread: `Workflow / name · description` and
`Schedules / name · description`. Neither has a leading decorative icon, and
the description truncates within the remaining Header width. Version history,
lifecycle state, owning Workflow/version metadata, and actions remain compact
trailing controls rather than a second line. Run detail uses the entry-context
contract below. Every focused Workflow, Schedule, and Run detail Header exposes
a leading localized back action to its owning collection; a Run opened from
Overview returns to Overview instead. The parent label remains readable
hierarchy rather than the only discoverable return control. `/` is reserved for
readable hierarchy, while opaque identifiers and unrelated metadata do not
become breadcrumb segments.
Collection filters visibly identify the filtered dimension when multiple
dimensions are present or the value would otherwise be ambiguous. A single
contextually obvious state filter may show only its current value to keep the
toolbar concise, but it still exposes the dimension through its accessible
name.

Binding creation presents Workflow, run schedule, required Workflow input, and
schedule details as one compact disclosure stepper in the
configuration column. Every applicable section heading remains visible, one
section may be expanded at a time, and collapsed sections retain a concise
summary and all draft state. A check mark means the Human has advanced through
that section and its current value remains valid; valid defaults in an unreached
section do not appear completed. Next validates and confirms the current section
before advancing. Confirmed sections remain freely reopenable, and later edits
retain confirmation while valid, lose completion while invalid, and recover it
without forcing the Human through unrelated sections again.

Creating a Schedule from Workflow detail uses a durable URL query containing
the current Plan ID. The create route validates that exact Plan as active and
publishable, preselects it, and binds the Schedule to that Plan's latest
published version without exposing version selection as a separate authoring
decision. A stale, unauthorized, archived, or unpublished target fails closed
and never silently substitutes another Workflow. Editing an existing Schedule
preserves its saved immutable version.

The Workflow selector remains operable when the active Workflow collection is
empty and offers one icon-labeled action that opens the Workflow list instead
of leaving a disabled dead end.

Changing Workflow makes required Plan input reviewable again only when the
latest version's exact user-facing input contract changed. Compatible values
remain in the current draft, independent trigger settings and details remain
confirmed, and
only the affected input section blocks creation until reviewed. Per-version
input drafts may be retained for the lifetime of the create form so switching
away and back does not discard typed values; they do not persist across
workspaces or become part of the Binding contract. A version with no required
non-default input omits the empty input section and advances directly from
run schedule to details.

The create action has one stable footer location. It becomes available after
the Human has reached the final details section, every preceding visible
section is confirmed, every current value is valid, no dependent section needs
review, and no contract load is pending. There is no duplicate header action or
separate confirmation page.

Validation and loading errors remain contextual below the affected schema-driven
control, and a failed create remains beside the create action. An error does not
reset the form, its current section, or any valid draft value.

Schedule detail offers an explicit edit action that reuses the same disclosure
stepper. It restores the saved Workflow version, schedule mode and values,
schema-driven inputs, metadata, and enabled state. Saving metadata uses the
Binding compare-and-swap contract; saving executable changes appends an
immutable revision and activates or disables it according to the restored
lifecycle control. Failed saves retain every entered value.

Binding detail keeps the Schedule collection and current Schedule in the
shared single-row Context Header. The owning Workflow is not a hierarchy
ancestor; its display name and pinned version remain linked trailing metadata
in that Header. The primary surface follows the shared management-detail
visual system and shows the Schedule's lifecycle, human-readable condition,
schema-labeled input values when present, and revision history. It covers every
declared schedule mode, recognizes common cron forms, and falls back to a
localized custom or unavailable condition without exposing raw configuration
as ordinary product copy.

Inputs use the pinned PlanVersion contract when it is available. Primitive and
workspace-backed values use type-appropriate presentation, while objects and
arrays stay summarized until explicitly expanded. Visible workspace-backed
values resolve their display names by exact resource identity; additional
values are resolved only after the Human explicitly expands the input list.
The page does not enumerate unrelated workspace resources. An unavailable
workspace-backed value is presented only as its selected resource kind; its
opaque identifier, the exact schedule source, and canonical input JSON remain
in a collapsed technical-details disclosure with copy controls. An empty input
map omits the entire Inputs section.

Trigger enable, disable, restore, archive, and revision activation remain
revision-checked server mutations. A failed mutation preserves the loaded detail
and reports a localized retryable error. Main-page reads are bounded to the
requested Binding, fixed-size revision and PlanVersion pages, its owning Plan,
at most the selected PlanVersion, and exact resources referenced by currently
visible inputs; they do not grow with unrelated workspace data. Expanding the
input list is the explicit intent that permits resolving additional referenced
resources.

Run and Schedule collection pages lead with human-readable Workflow and Schedule
names plus localized status labels and icons. Opaque Run, Invocation, Plan,
Binding, and revision identifiers are not primary list copy; they remain
available only where technical identity is required. The Run collection groups
loaded records by local calendar day, with newest days and newest rows first;
each day is a collapsible Section. A Run row is one keyboard-focusable,
whole-row detail target and does not repeat that navigation as a trailing
detail link. Elapsed time is quiet inline metadata before the source Badge.
Run status appears once as the shared leading status icon, with a
localized hover label and accessible description; it does not repeat as a
trailing Badge. Manual and Schedule sources use distinct icon Badges, and a
Schedule source shows the human-readable Schedule name. Schedule listing
obtains the owning Workflow display name in the bounded Binding list query
rather than issuing one Plan read per row. Its lifecycle control uses a state
icon and an inline switch immediately before the explicit detail action without
a redundant visible state label; archived Schedules expose their state without
allowing the switch to mutate them.

The Run collection and a Workflow detail's Run-history tab share the same row
presentation for Workflow identity, source, timestamps, localized status, and
whole-row detail navigation. Plan-scoped invocation data may omit source display
names, but this changes only available copy, not the row hierarchy or visual
system.

Run detail preserves the collection that opened it as its return context. Runs
opened from Overview use a leading back action that returns directly to
Overview, followed by the Run history label and the human-readable Workflow
name shown by the selected Overview entry. The Workflow name links directly to
that Workflow. Runs opened from Run history retain Run history as their
navigable parent and expose the same leading return action; direct Run links use
the same Run-history context. Every Run Header is a single row before and after
data loads, uses the Channel and Thread type hierarchy for
`Runs / Workflow name`, and does not append Workflow or source metadata as a
second line. No Run header uses an opaque Run ID or a generic `Workflow run`
label as its primary title. Run status uses the shared Workflow Run status icon.
Run content has two top-level views: Graph presents
the visual execution canvas, and YAML presents the pinned PlanVersion source.
The visual canvas remains mounted while the Human switches between these views
so its shared layout and fitted viewport do not flash or reset.
Initial Run loading preserves the same header, Graph and YAML tab row, content
region, resize boundary, and execution-detail column as the loaded page.
Lightweight skeletons occupy those stable regions instead of replacing the
detail surface with a generic loading message.

An `enum` declares at least one value. An `array` declares its item type. A
default must be assignable to its declared type. The v1 schema does not grow a
second JSON-Schema feature set or silently coerce quoted values into numbers or
booleans.

Invocation input is validated against the Plan input contract before a Run is
created. Each node activation receives an immutable input snapshot. A
successful action produces an immutable output snapshot validated against that
node's output contract. An explicit succeeded End maps data to and validates
the Plan output contract.

### 4.3 Graph structure

The graph declares:

- one `entry` node ID;
- nodes with stable IDs, a discriminated type, typed input/output contracts,
  type-specific configuration, and named ports;
- edges with stable IDs and explicit source and destination node/port
  endpoints. An author may omit a mapping only when the compiler can lower a
  type-safe same-name payload pass-through into explicit artifact bindings.

Node or edge list position is not identity. Type-specific `next`, `default`,
`onApprove`, `onReject`, or `onTimeout` pointers are forbidden. A missing
success/control edge does not complete a Run. Every successful path reaches an
explicit succeeded End. An author may route a failed/timed-out port to an
explicit failed End, or omit that error edge and let the compiler lower it to
a hidden fail-Plan sink. Multiple explicit success and failure Ends are
allowed.

Ordinary action nodes do not gain implicit parallel or choice semantics from
having multiple edges. Choice, conditional all-match emission, fixed
parallelism, dynamic WorkItem fan-out, synchronization, and cycles are
represented by their explicit graph constructs so the Builder and runtime
interpret the same graph.

### 4.4 References, expressions, templates, and mappings

The expression language remains deliberately bounded:

- string, number, boolean, null, and array literals;
- strict `==`, `!=`, `<`, `<=`, `>`, `>=`, and `in` comparisons;
- `&&`, `||`, `!`, and parentheses;
- `contains`, `startsWith`, `endsWith`, and `length`;
- `{{ expression }}` interpolation in message templates.

Compilation binds every expression to the typed context valid at that location.
Contexts distinguish Plan input, current activation input/output, typed Run
state, an edge's source value, fork branch data, dynamic `item.*`, fan-out join
`items`, and loop state/iteration. The source does not declare `event.*` or
`ctx.*`, and it does not expose a global `steps.<node>.output`: one static node
may have multiple concurrent or repeated activations, so a global latest output
is undefined.

An edge mapping constructs the destination input from the source activation's
typed input/output and the allowed graph context. Whole-output pass-through is
allowed only when the complete source output is assignable to the destination
input. Otherwise fields are mapped explicitly. Every required destination
field must have one deterministic source or a declared default.

Concurrent values are never merged by callback arrival order. A fixed fork
join exposes branch-keyed results; a dynamic fan-out join exposes source-ordered
terminal item envelopes. The author maps or renames those results explicitly.
No automatic shallow/deep merge or last-writer-wins behavior exists.

### 4.5 Action nodes

`loop.plan/v1` supports complete contracts for these action capabilities:

- `send_message`;
- `set_variable`;
- `wait`;
- `request_approval`.

Each action exposes documented completion ports, including success and the
applicable failed or rejected outcomes. Only `wait` also exposes a `timed_out`
outcome; `send_message`, `request_approval`, and `join` carry no authored
timeout, so their delivery, RPC, and database deadlines are private runtime
behavior and permanent delivery exhaustion fails. Authors may handle an error
port explicitly; an omitted failed/timed-out edge is compiled into a fully
explicit hidden fail-Plan route. Retry remains a bounded node policy. Transport
retries reuse the same durable effect identity; a node retry creates a new
attempt under the same activation and may not allow a late result from an older
attempt to win.

The retry-capable actions—`send_message`, `wait`, and `request_approval`—may
omit the entire retry mapping; canonical compilation then supplies
`{maxAttempts: 1, backoff: PT1S}`. When retry is present, both fields are
required.

#### `send_message`

`send_message` contains exactly one destination plus a message template and
sends through the normal Loop message/fanout path:

- `target` is the legacy and author-friendly target string for a destination
  that still needs resolution, including Channel/DM/Task addresses and Team
  pseudo-targets;
- `scope` is a typed value expression that must resolve to the canonical
  concrete `scope` value described in §4.2.

Missing both destinations or declaring both is invalid. A target string is not
coerced into a scope, and a scope never accepts the Team pseudo-target. After
either form resolves, the message writer loads the committed scope, verifies
active state, workspace, and kind, and reauthorizes the Run's frozen execution
owner through the ordinary Human message path. Destination resolution and
message creation share the existing durable effect reservation, so retries do
not create a second logical message.

The typed compiler rejects `agent` or `array<agent>` values interpolated into
`target`, whether bare or embedded in an address such as
`dm:@{{ input.worker }}`. Agent identity selects callback execution through
`responder`; it does not identify the delivery destination. Authors use a
concrete `scope` or a valid Channel, DM, Task, or Team target for delivery and
declare `responder` independently.

The message may mention an Agent. Callback mode may instead declare one typed
`responder` expression resolving to an `agent`. Effect preparation resolves the
expression to one Agent and materializes that Agent's canonical routing token
as a real visible mention in the Workflow message. The ordinary message
transaction parses the mention and performs the same durable fanout,
notification, and wake routing as any other live mention; there is no hidden
direct-recipient delivery path. The accepted callback receipt freezes the one
Agent resolved by that mention. The resolved identity is part of the effect
idempotency digest, so replay cannot retarget after Team or Agent membership
changes. Omitting `responder` preserves the authored-text mention callback
route. `responder` is invalid for `sent` completion and never changes a Task
assignee.

Completion mode is explicit:

- `sent`: completion follows durable message acceptance and effect recording;
- `callback`: after durable send, the activation waits for a structured result
  through a server-controlled, attempt-scoped callback command/API.

Sent mode may set `startThread: true` when its destination is a Channel root.
The writer then reuses the ordinary `StartAgentThread` message option to commit
the visible root message and empty child reply Thread in one transaction.
Authored DM, Task, Team, and existing reply-Thread destinations are invalid;
dynamic targets and scope values are checked again after committed resolution
and before the writer starts the transaction.

The compiler owns the completed output contract for this mode:

```yaml
messageId: { type: string, required: true }
threadScope: { type: scope, required: true }
```

`threadScope` is the canonical child Thread identity. A downstream node binds
`output.threadScope` across an edge into a scope-typed input; Plan source and
Agent callback output cannot declare or override either reserved field.
`deliveryScope` continues to mean the parent Channel and is not repurposed.
`startThread` is incompatible with callback completion: a staged flow first
opens the Thread without an Agent mention, then addresses a downstream callback
node by the trusted `threadScope` and selects its Agent with `responder`.
Runtime materializes that selection as the canonical visible mention.

Callback mode may declare typed business output. `send_message` carries no
authored timeout; its delivery, RPC, and database deadlines are private runtime
behavior, and permanent delivery exhaustion fails. The author must route
`completed`; failed routing may be explicit or use the compiler-generated
fail-Plan route. A callback node whose only result is its compiler-owned
`workScope` may
omit business `callbackOutput`. The callback capability binds the exact Run,
activation, and attempt; it is runtime data, never author-supplied source. The
server validates and persists callback output before activating an outgoing
edge. Duplicate callback submission is idempotent. A superseded,
cancelled, or terminal attempt cannot be revived by a late callback.

A callback `send_message` may opt into the Agent collaboration contract:

```yaml
sendMessage:
  scope: { ref: input.dispatch_scope }
  responder: { ref: input.worker }
  text: "Implement the requested change"
  completion: callback
  workScope: null
  relatedScopes:
    - { ref: input.requirements_scope }
    - { ref: input.related_task }
  callbackOutput:
    summary: { type: string, required: true }
```

The three collaboration fields have distinct meanings:

- `deliveryScope` is trusted runtime receipt data identifying where the
  Workflow message actually woke the Agent. It is not author-supplied and is
  not implicitly reused as the Agent's work location.
- `workScope` identifies the one collaboration space in which the Agent
  organizes and records this activation's work. An authored concrete
  scope-valued expression fixes that identity. An explicit `null` delegates the
  choice to the Agent, which may reuse an accessible scope or create one through
  an ordinary domain operation. Omitting the field entirely retains the legacy
  callback contract and is different from authoring `null`.
- `relatedScopes` is an unlabelled collection of existing collaboration spaces
  that provide context for the work, with at most 64 entries. It may be
  authored as a list of scope-valued expressions or, in the v4 contract, as one
  expression resolving to required `array<scope>`. The runtime canonicalizes
  and de-duplicates the concrete values without assigning roles or control-flow
  meaning. The collection is trusted references for on-demand access through
  ordinary domain tools; it neither grants access nor causes automatic history
  hydration.

`relatedScopes` may be authored only when `workScope` is present, either as a
concrete expression or explicit `null`. A related-only declaration is invalid,
including `relatedScopes: []`. Both fields are valid only with callback
completion. At activation, the work-scope mode, any fixed work scope, and the
concrete related scopes are frozen in the durable effect request.
`workflow step get` returns them alongside `deliveryScope`. The boundary
must prove that both the authenticated callback Agent and frozen Human
execution owner can write the fixed or Agent-selected `workScope`; every
`relatedScope` requires visibility for both actors but not work/write access.
Scope existence, active state, workspace, kind, and authorization are checked
again at the consuming boundary. Related scope IDs are exposed only through the
authenticated callback context, not visible message metadata.

When the concrete `workScope` is a Task, runtime uses the existing Task watch
state in two v4 collaboration cases. Every structured callback whose delivery
scope and fixed `workScope` are the same canonical Task watches that Task for
its responder when the callback activates; this includes ordinary static
Planner/reviewer callbacks as well as WorkItem callbacks inside `fan_out`. A
callback with Agent-selected `workScope` watches the selected Task when
successful completion first freezes it. This watch is notification state, not
a Task claim, assignment, execution lease, alternate Task-routing channel, or
second assignee. Plans and callback instructions must not require the Agent to
claim the Task merely to answer the callback.

Watch handoff follows a whole-Run causal frontier. Every callback activation
with the same canonical fixed Task delivery/work scope contributes one Task
handler token. Ordinary edges carry that frontier, forks copy it to every
selected branch, joins union the surviving branch frontiers, and `fan_out`
copies/merges the same frontier across dynamic WorkItem dependency edges. When
a later same-Task callback starts, it replaces only the tokens on its own
causal input. Runtime first watches the Task for the successor Agent and then,
in the same ordinary Task-message transaction, unwatches only predecessor
Agents whose tokens disappeared from the whole live frontier.

Consequently, `A -> B` transfers the watch from A to B. Independent branches
`A -> B` and `C -> D` transfer A to B and C to D independently, leaving B and D
watched. For a common predecessor fork `A -> (B, C)`, the first successor does
not remove A while the other branch still carries A; A is removed only after
the last branch replaces it. A later callback after the join replaces every
same-Task token merged by that join. Exact canonical Task scope equality is
required, so no cross-Task handoff is inferred. Failed delivery before a
responder was watched restores the predecessor frontier. That restored token
remains the final handler for its uncovered causal branch: a parallel sibling
cannot replace it, but a later same-Task callback after the branches join can
replace both it and the sibling's token. A delivered callback that exhausts its
private delivery budget preserves the receipt's frozen responder identity for a
later handoff.

Runtime persists at most one newly projected fixed-Task callback until its
ordinary Task-message delivery receipt has been observed in Run state. This
serializes only the short message-transaction confirmation window; once each
receipt is committed, callback Agents that target different (workspace, Agent)
pairs still execute concurrently.

#### Platform presentation and callback recovery

Platform-generated fixed copy has one server-owned, versioned presentation
contract. The persisted message `content` and an interactive card's
`FallbackText` remain complete canonical English and are the compatibility
fallback for older clients, APIs, search, exports, Agents, and daemons. A
presentation envelope never replaces or rewrites those values.

The reserved `messagePresentation` metadata value is an object of the form:

```json
{
  "version": 1,
  "key": "workflow.recovery.nudge",
  "args": {
    "runId": "wri_...",
    "stepId": "wfx_...",
    "effectId": "wfx_...",
    "attempt": 1,
    "totalAttempts": 5
  }
}
```

The server allowlist currently contains only
`workflow.recovery.nudge`, `workflow.run.approval.pending`,
`workflow.run.approval.approved`, and `workflow.run.approval.rejected`. Each
key has a closed typed argument object. Recovery arguments contain trusted Run,
Step/effect selectors and bounded 1-based attempt values. Workflow Run approval
arguments contain trusted Plan, PlanVersion, Run, node, and the authored
approval summary as a dynamic string. The summary is never interpreted as a
translation key. Unknown or missing versions, keys, fields, argument types, or
bounds are invalid and clients render canonical English; they never display a
key or translate arbitrary metadata. The metadata key is reserved from ordinary
message authors and is carried unchanged through message-list, realtime,
reply-thread, and browser DTO projections.

Only `RecoveryRunner` adds the recovery envelope. It continues to render the
same English recovery prompt, select the frozen callback delivery scope (thread
when present, otherwise delivery scope), preserve the frozen responder and
authority, and use the deterministic
`workflow-run-recovery:<run>:<effect>:<attempt>` client message ID. Recovery
messages remain ordinary callback-wake messages: callback, Agent wake,
consumption, attempt, and idempotency semantics are unchanged.

Workflow Run approval cards carry the same envelope in their card render
payload. Pending, approved, and rejected title/status/action copy is localized
only through the allowlisted Workflow card keys; malformed or unknown card
values use the producer's English title, labels, status, and fallback. The
summary, markdown, body, and field values authored by a Plan or Agent remain
verbatim dynamic content. Team Workflow Plan approval cards use their own
existing contract and are not Workflow Run approval cards.

The browser uses the effective viewer locale, supporting `en`, `zh-CN`, and
`ja`; unsupported or missing locales resolve to `en`. Locale changes rerender
already loaded messages from their immutable envelope and canonical fallback,
without a server rewrite or refetch. Canonical-content operations such as
copy, edit, API reads, search, and export continue to use the English content.

### Workspace+Agent execution capacity and FIFO queue

A callback Agent is executed with **capacity=1 per (workspace, canonical
Agent)** across every Plan, Run, static callback, and fan-out WorkItem. Before a
`send_message.callback` effect is delivered, the dispatcher resolves the
canonical responder Agent through the same trusted AgentRef/destination chain
the ordinary message transaction uses and freezes it on the effect. It then
atomically acquires the durable `workflow_agent_execution_leases` row or
enqueues the effect in the stable FIFO `workflow_agent_callback_queue`:

- Only the FIFO head may acquire the execution lease and deliver. Every later
  callback for the same (workspace, Agent) stays durably `queued_for_agent`,
  distinct from the delivered `awaiting` state, with a frozen enqueue order that
  never moves on retry, restart, rename, or membership change. Candidate scans
  and effect claims return only the true active head per (workspace, Agent) —
  never a batch of tails — so a bounded claim-limit scan cannot starve a
  higher-enqueue-seq head.
- The **frozen responder is the sole authority**. Once
  `callback_responder_agent_id` is persisted, queued promotion, unbound retry,
  worker-lease reclaim, and crash recovery reuse that exact ID and never
  re-resolve a mutable AgentRef. The exact frozen Agent ID is passed into the
  ordinary message transaction as an internal trusted fence: the transaction
  compares the canonically resolved responder before any message/outbox/fanout/
  wake/Task-watch write and rolls back with zero visible side effects on
  mismatch, so an alias/visibility drift can never write B's message while
  holding A's queue/lease. Bound receipt recovery trusts the durable receipt and
  verifies it against the persisted effect responder.
- The release transaction marks the lease released and the queue item terminal
  in the same atomic write and does NOT poke another Run; the effect dispatcher
  polls the durable queue head each scan, so promotion is prompt without any
  cross-Run lock (which would form a lock-order cycle with a concurrent claim).
- A bounded, workspace-group-scoped reconcile runs on each worker pass: a held
  execution lease whose owner Run or callback effect is provably terminal (or
  whose owner effect row is gone) is released and its queue item terminalized so
  the next FIFO head can run. It is idempotent and never releases a live owner:
  awaiting, pending-retry, and dispatching/reclaiming callbacks are preserved
  regardless of age. A reconciler reports only the rows its own UPDATE actually
  flipped, so concurrent reconcilers scanning the same held lease never
  double-count a release (the lease release and queue terminalization are one
  atomic transaction).
- Different Agents in the same workspace stay parallel, and the same Agent in
  different workspaces never blocks. `fan_out.maxConcurrency` still bounds
  lineage; it does not replace the workspace+Agent lease.
- The execution lease is held from before the first delivery attempt until the
  callback completes, fails, or is canceled. Retryable delivery errors
  (including lost writer responses recovered from the bound message receipt)
  keep the same owner and FIFO position and never double-deliver. Ordinary short
  worker dispatch-lease expiry never releases execution capacity.
- Legacy `awaiting` callbacks already frozen to an Agent are active barriers:
  no new callback is dispatched to that Agent until every one is terminal.
- Only Agent callback effects hold an execution lease; `sent` messages, system
  reducer nodes, `fork`/`join`/`fan_out` control nodes, `wait` timers, and
  Human approvals do not.

The receipt remains authoritative if the Agent completes immediately or the
activation is subsequently canceled, while a terminal pre-delivery result with
no message receipt restores rather than invents a responder.

The native message transaction also binds the visible message to its immutable
Run effect. If that transaction commits but the writer response is lost, the
binding proves that delivery may already have changed Task watch state. The
dispatcher therefore reconstructs the canonical receipt directly from the
durable message/outbox record—even after the private delivery budget or
retry budget—without re-running the writer or consulting mutable owner,
membership, destination, or routing state. A bound message effect cannot
terminalize as a message-less failure. After receipt recovery, the private
delivery budget may still exhaust, but permanent delivery exhaustion is a
Workflow failure rather than an authored `timed_out` route, and the frozen
responder remains in the causal frontier for a later handoff.

This is Workflow/runtime bookkeeping only. Agents do not claim the Task or
interpret a new handoff protocol. Ordinary Task discussion continues through
the existing Task watch message path, and the implementation does not add a
lease, a custom Task router, a Workflow-only message channel, or a
multi-assignee Task model.

When authored `workScope` is `null`, a successful Agent callback must bind one
canonical scope in the top-level `complete.workScope` field. The Workflow never
infers this choice from tool calls, message destinations, or newly created
Tasks. When the authored value was concrete, the callback may omit
`complete.workScope`; if it supplies a value, that value must exactly match the
frozen scope. Failure does not require a work scope. `relatedScopes` are input
context and are never returned by the Agent.

The compiler owns required `workScope: scope` on the callback node's completed
output. Runtime fills it from the frozen authored scope or the successful Agent
binding, so a downstream reviewer, retry, or QA node binds
`output.workScope` across an ordinary edge. Plan source cannot redeclare or
override this reserved field in `callbackOutput`. Completion, business output,
final work scope, authorization, and idempotency conflict detection share the
same first-result-wins transaction; a replay with a different work scope is not
idempotent.

A Planner callback opts into runtime-sized work with
`sendMessage.workItems.allowedResponders`. `allowedResponders` is exactly one
typed expression resolving to a non-empty `array<agent>` and is frozen in the
callback intent. It is an authorization bound, not a dispatch strategy:

```yaml
sendMessage:
  scope: { ref: input.planning_scope }
  responder: { ref: input.planner }
  text: "Split this request into executable work"
  completion: callback
  workScope: { ref: input.planning_scope }
  relatedScopes: { ref: input.context_scopes }
  workItems:
    allowedResponders: { ref: input.workers }
  callbackOutput:
    plan_summary: { type: string, required: true }
```

The successful callback supplies `workItems` at the top level beside `output`
and optional `workScope`; `workItems` is reserved and cannot appear in
`callbackOutput`. When the node declares `workItems`, completion must supply
the field, although `[]` is valid. The callback's business fields remain only
inside `output`.

Every returned WorkItem has this exact closed shape:

```json
{
  "key": "implement_api",
  "dependsOn": ["design_contract"],
  "workScope": { "kind": "task", "scopeId": "tsk_api" },
  "relatedScopes": [{ "kind": "task", "scopeId": "tsk_parent" }],
  "responder": "agt_backend",
  "instruction": "Implement the frozen contract."
}
```

`responder` is a union with exactly two mutually exclusive forms: a fixed
canonical agent string, or an ordered selectOne candidate pool that picks the
first currently available agent:

```json
"responder": {"selectOne": ["agt_backend", "agt_ml", "agt_frontend"]}
```

A selectOne pool is deduplicated preserving first-occurrence order and must be
non-empty and at most 64 candidates; every candidate must be a canonical agent
inside the frozen allowlist. The same `responder` union is exposed on the
`item.*` source port of `fan_out` and consumed by the item body's
`input.responder`, which must be declared `type: responder` so it accepts both
a fixed agent and a selectOne pool. A `responder` value is never treated as a
plain `agent` string; an `agent`-typed field remains strictly a single agent
string.

`responder.selectOne` supports an additive `strategy` knob. Omitting it (or
declaring `strategy: ordered`) keeps the legacy strict-priority behavior: the
first currently available candidate in pool order wins, and FIFO waiting /
promotion / recovery are unchanged. Declaring `strategy: random` selects
uniformly at random among the currently available deduplicated candidates
(those with free callback capacity and not reserved by an older waiter) using
an injectable unbiased randomizer (a crypto-backed source in production). The
ordered (omitted) strategy remains strictly ordered; the random strategy is
only valid alongside a selectOne pool with at least two candidates.

```json
"responder": {"selectOne": ["agt_backend", "agt_ml", "agt_frontend"], "strategy": "random"}
```

`key`, `workScope`, `relatedScopes`, `responder`, and non-blank `instruction`
are required. `dependsOn` is optional and canonicalizes to `[]`. Unknown
fields, duplicate or non-canonical keys, duplicate dependencies, self or
unknown dependencies, dependency cycles, non-canonical scopes, more than 64
related scopes, an empty selectOne pool, duplicate selectOne candidates, more
than 64 selectOne candidates, and responders outside the frozen allowlist are
invalid. The batch contains at most 256 WorkItems and is validated, authorized,
and persisted atomically. Across the batch, at most 2,048 dependency edges, 512
related-scope references, 6,144 canonical JSON values, and 1 MiB of canonical
WorkItem JSON are accepted. The final business callback output plus
compiler-owned WorkItems must also fit the ordinary durable callback-payload
budget. No prefix can become runnable if another item is invalid.
The completed node exposes the validated nominal value as compiler-owned
`output.workItems: work_items`.

The Planner remains responsible for ordinary domain choices. It may use Task
tools before completion to reuse a scope or create one or more child Tasks and
then return those exact scopes in the WorkItems. Workflow execution does not
infer WorkItems by scanning a Task tree, does not infer a selected scope from
tool calls, and does not mutate Task assignees. The Planner must likewise leave
a newly created WorkItem Task unassigned and leave a reused Task's assignee
unchanged: `WorkItem.responder` owns execution selection, while Workflow
runtime owns Task-watch continuity. A Task create made during the active
callback may carry a model-authored `idempotencyKey`; the daemon binds
that key to the hidden Run/callback/Agent identity. An exact key-and-payload
retry returns the original Task IDs even after callback completion, while a
different payload conflicts and a key outside an active callback is rejected.

Workflow-produced messages use a first-class Workflow avatar and a badge with
the frozen Plan display name in both realtime and history views. They do not
show a synthetic "runs as" identity label and do not open a human profile from
the Workflow avatar. The immutable execution owner remains the underlying
audited author used by the ordinary message transaction; it is not exposed as
the Workflow's visible identity and cannot be supplied by Plan source.

#### `set_variable`

`set_variable` writes declared typed Run state and exposes its compiler-visible
read/write set. It cannot create undeclared paths or write Plan input, node
output, fork metadata, or loop metadata. The compiler rejects parallel state
access that can race or produce arrival-order-dependent results. Runtime writes
use state version/CAS protection and preserve before/after audit data.

#### `wait`

`wait` persists a condition and optional deadline and then releases its worker.
It resumes from a durable state change or timer reservation. It must not hold a
worker, goroutine, or database transaction while waiting. `wait` is the only
action node that supports an authored `timeout` and a `timed_out` route.

#### `request_approval`

`request_approval` persists one attempt-scoped approval request and routes
approved and rejected outcomes through explicit ports. Failed outcomes may be
routed explicitly or use the compiler-generated fail-Plan route; it carries no
authored timeout, so delivery/RPC/database deadlines are private runtime
behavior and permanent delivery exhaustion fails. Decisions are authenticated
and idempotent; an older attempt cannot
decide a newer request.

An approval may optionally declare `feedbackScope`, using the same typed
`ValueExpr` semantics as `sendMessage.scope`:

```yaml
requestApproval:
  assignee: "{{ input.approver }}"
  summary: "Approve this change"
  feedbackScope: { ref: input.feedback_scope }
```

The expression must resolve to a required canonical scope object with exactly
`{kind, scopeId}`. `kind` is one of `channel`, `thread`, `dm`, or `task`; target
strings, aliases, unknown fields, `null`, and other result types are rejected.
A literal is normalized and validated through the domain scope contract, so
field order is immaterial but extra fields and non-canonical IDs are invalid.
The reducer evaluates `feedbackScope` once when the approval activation is
created and writes the concrete snapshot to the durable Intent and effect
request. Dispatch passes that same value to `ApprovalRequest`; the decision
sink returns it from the persisted effect request in `ApprovalDecisionResult`.
It never re-reads mutable Plan source or accepts a client-provided feedback
scope. Omitting `feedbackScope` preserves the legacy Plan/artifact bytes,
request wire shape, and behavior. The feature uses the next immutable artifact
version; older artifact readers reject it rather than silently dropping the
field.

When `feedbackScope` is present, the Approval App card declares the same
optional bounded `comment` input on both Approve and Reject. The trimmed comment
is part of the first durable decision and is never treated as the generic
Reject `reason`; an empty comment does not post a message. After the decision,
a non-empty comment is sent by the approving Human through the ordinary scoped
message/fanout path using the frozen scope and a decision-derived stable client
message id. A retry may replay the decision or card mutation, but it cannot
create a second comment or replace the first comment. Legacy approvals without
`feedbackScope` retain zero-input one-click actions.

### 4.6 Switch and conditional routing

Switch declares one explicit mode:

- `first_match` evaluates cases in deterministic order and emits only the first
  matching port;
- `all_match` evaluates all cases and emits every matching port in parallel.

Every switch has exactly one default case. `first_match` selects it when no
earlier condition matches; `all_match` selects it only when no conditional case
matches. Conditions belong to ordered switch cases that emit named ports; they
are not hidden control flow on ordinary action edges. Conditions are pure,
side-effect-free, type-checked expressions over the source context. The switch
mode makes conditional choice or conditional parallel routing explicit and
remains distinct from an unconditional fork. An
`all_match` emission creates conditional fork lineage and records the actual
matched branch set so a downstream join can correlate and wait for that exact
set.

### 4.7 Parallel and join

A fork node creates one child token for every declared parallel branch. Every
branch has a stable branch key. Tokens retain fork activation and branch lineage
through nested nodes and loops. Unlike `switch: all_match`, a fork does not
select branches by condition; it unconditionally activates its declared branch
set.

A join explicitly names its owning fork source—an unconditional fork or an
`all_match` switch—and selects one mode:

- `all`: wait for one successful arrival from every branch activated by that
  particular fork activation;
- `any`: atomically accept the first successful arrival, close the join, and
  cancel the remaining branches of that fork activation. `any` always means
  first-success, never first-terminal.

Join correlation is runtime lineage, not author data. The effective key includes
Run, join node, and fork activation identity, so different loop iterations and
nested forks cannot mix. An `all` join fails according to its explicit failure
route when a required branch cannot succeed. An `any` join fails only when no
branch can succeed. Cancellation of losing branches stops new graph work and
invalidates their callbacks; it cannot retract an already accepted external
message.

An `all` join exposes results under stable branch keys. An `any` join exposes
the winning branch key and value. Downstream merge is always explicit.
Fork/join scopes are properly nested, and a branch cannot arrive twice at the
same join for one fork activation.

#### Dynamic WorkItem fan-out

A `fan_out` consumes one required `work_items` value and instantiates one
compiler-proven static item body for every frozen WorkItem:

```yaml
- id: dispatch
  type: fan_out
  input:
    work_items: { type: work_items, required: true }
  fanOut:
    items: { ref: input.work_items }
    maxConcurrency: 2
    failureMode: collect
```

`maxConcurrency` is optional; compilation freezes the default `4`. An authored
value is an integer from 1 through 64 and limits complete in-flight item
lineages, including waits, callbacks, retries, and nested static nodes. It is
not only a limit on the first message effect. `failureMode` is currently
exactly `collect`; there is no fail-fast, quorum, or first-success mode.

The `item` source port has one closed compiler-owned context:

- `item.key`, `item.dependsOn`, `item.workScope`, `item.relatedScopes`,
  `item.responder`, and `item.instruction` are the frozen authored WorkItem
  fields. `item.responder` carries the responder union: a fixed canonical
  agent string or a `{selectOne: [ordered agents]}` pool. An item body that
  forwards the responder declares `input.responder: {type: responder}` and
  binds `responder: {ref: item.responder}`; the runtime materializes a fixed
  winner for delivery and Task watch exactly as a directly-authored
  sendMessage selectOne does;
- `item.dependencies` is an object keyed by direct dependency key. Each value
  is that dependency's terminal result envelope, with nested `dependencies`
  deliberately omitted so a deep DAG cannot recursively duplicate its result
  graph. The envelope's `value` field is the object bound into the closing
  join's item input. It is not the action's raw `output` namespace. For
  example, when the body-to-join edge binds
  `result: {ref: output.result}`, the direct dependency business result is
  available at `item.dependencies.<key>.value.result`; an `output` field is not
  present in that envelope.

The scheduler visits WorkItems in callback source order. It starts a pending
item only after every direct dependency succeeded and while a concurrency slot
is available. An item body that reaches its paired join records `succeeded`.
An omitted body error route, an explicit failed End, or a timeout records the
corresponding terminal envelope for that item rather than failing the entire
Plan immediately. Independent items continue. A dependent of any unsuccessful
item becomes `blocked`; `blockedBy` is the source-ordered set of unsuccessful
root blockers propagated through the DAG.

Every `fan_out` has exactly one closing `join`:

```yaml
- id: collected
  type: join
  input:
    result: { type: object, required: true }
  join:
    fanOutId: dispatch
    mode: all
    result:
      results:
        type: array
        required: true
        items: { type: object, required: true }
    merge:
      results: { ref: items }
```

`fanOutId` and `forkId` are mutually exclusive, and a fan-out join uses
`mode: all`. The `items` merge space is the source-ordered array of terminal
WorkItem envelopes, not callback-arrival order. The join becomes ready exactly
once after all items are terminal. Zero WorkItems therefore resolve immediately
to an empty array and still activate the downstream reviewer exactly once.

The compiler proves one entry, no external body ingress, one closing join, and
closed success/failure boundaries for the static item body. Run state writes
inside that body are rejected because concurrent instances would race. Runtime
persists the complete frozen item list, deterministic semantic item IDs, each
item's single terminal business value, lineages, statuses, and join activation
before exposing work, and validates that state again after recovery before
scheduling. Direct dependency envelopes are projected on demand from those
immutable predecessor values and frozen only in the launched consumer's root
activation input; they are not copied onto every WorkItem dependency edge.
Replay cannot add, remove, reorder, or retarget WorkItems.

The v4 surface deliberately rejects a `fan_out` nested inside another
`fan_out`. It also omits conditional run-after-failure dependencies, per-item or
per-responder concurrency policies, a second dynamically returned batch, and
an Agent-authored graph mini-language. Those require future explicit source and
artifact contracts rather than runtime inference.

### 4.8 Counted loop

Cycles exist only inside an explicit counted loop scope. The sole v1 loop
configuration is `mode: counted` with a positive `iterations` value; unbounded
or condition-controlled loops and `maxIterations` exhaustion semantics are not
part of `loop.plan/v1`.

A counted loop defines typed state, a `body` source port, a required incoming
`continue` port, an optional incoming `break` port, and a `done` source port.
Entering the loop emits `body` for iteration 1. Each `continue` atomically
replaces the state. Before the requested count it advances the iteration and
emits `body` again; on iteration N it automatically succeeds and emits `done`.
Consequently `iterations: 5` means exactly five body traversals, not a failure
safety limit. Final state is the successful result and can pass directly to a
succeeded End without a duplicated `result` contract. `break`, when connected,
is an early successful exit and uses the same state/result contract.

Each iteration has distinct loop lineage and each visit to a static node
creates a new activation. A parallel created in a loop iteration therefore
receives a new fork activation, and its join cannot consume tokens from another
iteration. A break or final continue cannot leave unjoined parallel work or
another open graph scope behind. Back-edges and self-loops outside an explicit
loop scope are compile errors.

### 4.9 End nodes

End is an explicit control node with `succeeded` or `failed` status. Every Plan
has at least one explicit succeeded End, which maps and validates Plan output.
An explicit failed End records a stable error code and bounded detail suitable
for user display and operator diagnostics; it is optional when all unhandled
errors use compiler-generated fail-Plan sinks.

One token reaching End does not complete a Run while another live token or open
fork/fan-out/loop scope exists. The compiler rejects graphs whose terminal
semantics could strand live work.

## 5. Compiler contract

Publishing and explicit validation use the same deterministic, multi-pass
compiler:

1. strict YAML parse and header validation;
2. type, constraint, and discriminated node/action schema validation;
3. node, edge, endpoint, port, and branch-key symbol resolution;
4. typed expression and template compilation;
5. edge mapping and whole-output assignability checks;
6. entry, reachability, routing, implicit error lowering, and End analysis;
7. switch first/all-match, order/default, and unconditional fork checks;
8. fixed fork/join dominance, correlation, nesting, and branch completion
   analysis;
9. dynamic fan-out entry/body/join scope, WorkItem context, and nesting
   analysis;
10. counted-loop and strongly connected component ownership analysis;
11. parallel and fan-out Run-state read/write conflict analysis;
12. timeout, retry, callback, and outcome-port consistency checks;
13. lowering to canonical versioned IR and semantic digest.

At minimum, compilation rejects:

- unknown fields, unsupported types, and invalid defaults;
- duplicate or unstable node/edge IDs;
- dangling nodes, edges, ports, references, or branch keys;
- missing, unreachable, or multiply defined entry;
- unreachable nodes and success/control paths without explicit terminal
  behavior;
- ambiguous ordinary branching, missing switch default, or invalid condition
  order;
- incompatible or incomplete input/output mappings;
- references outside the statically valid context;
- fork/join mismatches, cross-scope joins, duplicate branch arrival, and joins
  that can wait forever for an unactivated or bypassed branch;
- missing or multiply paired fan-out joins, external item-body ingress, open
  item paths, nested fan-out, non-`collect` failure mode, and fan-out body state
  writes;
- cycles outside loops, non-positive counted iterations, and loop scope leaks;
- parallel state effects that can race;
- partially specified callback delivery policy;
- implicit success/control completion through a missing edge or magic
  sentinel.

Diagnostics are an aggregate list, not a first-error-only response. Each finding
has stable code, severity, document path, source line/column, message, and the
applicable node or edge ID. Error severity blocks publish. Warnings may identify
safe but surprising authoring choices and do not change runtime behavior.

The compiled IR is an explicit wire artifact. It contains compiler version,
digest, normalized typed contracts, node opcodes/configuration/ports, edges,
conditions, mappings, fork/fan-out/join/loop scope metadata, and state effects.
It does not duplicate the entire source document, depend on Go's default JSON
field names, retain raw expression/template source as runtime instructions, or
require the runtime to reinterpret YAML.

Artifact evolution is reader-first. Plans that use neither `scope` contracts,
`sendMessage.scope`, nor `sendMessage.startThread` continue to emit
`loop.plan.artifact/v1` with the v1 compiler envelope and retain their existing
canonical bytes, empty sent output, and digest. A Plan that uses at least one
of those v2 features while using neither `sendMessage.workScope` nor
`sendMessage.relatedScopes` emits `loop.plan.artifact/v2`; v2 adds the scope
value type, the destination union, and the trusted Thread-start output
contract. Any valid authored `sendMessage.workScope`, with optional
`sendMessage.relatedScopes`, opts the Plan into the Agent collaboration
contract and emits `loop.plan.artifact/v3`; v3 carries the closed work-scope
mode, optional fixed scope expression, related-scope expressions, and
compiler-owned completed output. A Plan that uses
`sendMessage.responder`, an array-valued `relatedScopes` expression,
`sendMessage.workItems`, nominal `work_items`, `fan_out`, or
`join.fanOutId` emits `loop.plan.artifact/v4` with
`loop.plan.compiler/v4`; v4 adds the selected-responder mention intent,
dynamic WorkItem contract, compiled fan-out scope, item/join expression spaces,
and frozen concurrency policy. All four artifacts preserve the same
`loop.plan/v1` source header.
The decoder accepts every immutable envelope, validates each against its own
closed field/type surface, and never interprets newer fields under an older
envelope. In particular, published v2 Plans that used an ordinary
`callbackOutput.work_scope` continue to execute as generic typed business
output; new authoring does not use that convention.

The Thread-start effect keeps the normal stable `clientMessageId`. If a process
crashes after the root/child transaction commits but before effect resolution,
replay reads the persisted scope topology and returns the same root message and
child `threadScope`; the canonical effect-result bytes are identical and no
second root or Thread is created.

The semantic digest is the SHA-256 hash of a domain-separated canonical
semantic projection of that IR. The projection includes every value that can
change execution and excludes the digest itself, compiler/artifact/source
envelope versions, visual layout, and authoring-only labels or descriptions.
Equivalent YAML whitespace, key ordering, and—only where the DSL defines both
forms equivalently—omitted-versus-empty authoring collections therefore
produce the same digest. Any executable contract, opcode, expression, mapping,
policy, or scope change produces a different digest.

## 6. Builder contract

The visual Builder and YAML editor are two editors over the same server draft:

- visual changes update explicit nodes and edges rather than type-specific
  transition fields;
- the visual toolbar exposes one searchable, categorized Node picker for every
  directly addable v1 Node type and a document-local Node finder. The picker
  and direct-edit flow present addable Nodes as steps and describe only
  behavior the current Node contract can perform, rather than leading with DSL
  or compiler terms.
  Selecting a finder result selects and centers that existing Node without
  performing a server read. The same Node picker is reused on an eligible
  connection so the Human does not need to delete and rebuild a simple path
  merely to add an action. Adding or inserting a Node selects and centers it and
  opens its direct configuration;
- connection insertion is an atomic local-Draft graph mutation and is exposed
  only where it cannot silently reinterpret data. The current direct insertion
  set is `send_message` and `wait` on an ordinary connection with no bindings
  whose target declares no input fields. Bound connections, typed-input
  targets, Entry, control-flow Nodes, Join, Loop, fan-out, End, state writes,
  and branching approvals continue to use explicit graph editing or AI edit.
  The insert affordance stays quiet until the connection is hovered or
  selected;
- YAML edits preserve the last valid visual graph while showing source-linked
  diagnostics;
- layout is saved as an authoring sidecar keyed by stable graph identity and is
  excluded from executable source and digest;
- automatic layout is an explicit Builder mode represented by a switch.
  Enabling it arranges and fits the current graph and reapplies the arrangement
  when graph topology changes. Nodes are not draggable while the mode is
  enabled. Disabling it preserves the current positions, immediately restores
  manual node dragging, ignores any unfinished layout result, and must not
  trigger later fitting or rearrangement;
- the latest-save metadata follows the Workflow description after a faint
  vertical separator, uses only a restrained hover/focus affordance, and opens
  change history directly with the latest immutable snapshot selected. This
  focused history mode stays inside the same Builder, replaces the first-level
  header with `Back to latest` and a primary `Restore this change` action, and
  removes the normal second-level detail tabs and authoring
  actions, keeps only visual/YAML navigation and zoom/pan, renders the selected
  immutable version read-only in the existing viewport, and temporarily
  replaces the Inspector with the cursor-paginated version list. It never
  creates a second canvas or exposes source, layout, or compiled-artifact JSON
  as a separate product surface;
- change-history rows show the save timestamp and editor display name, never a
  technical ID, generated ordinal, or overflow menu. The current row is marked
  `Latest`; selecting any row previews it in place. Restoring an eligible
  historical row requires explicit confirmation and appends it as a new
  immutable latest change without mutating prior history or existing
  Schedules;
- leaving history mode through `Back to latest` or Escape
  restores the same draft session, including unsaved source/layout, viewport,
  selected canvas member, and dirty state. Navigating to another Workflow
  clears the preview state so it cannot leak across routes. Historical preview
  cannot mutate the draft;
- archived Plans are readable but authoring controls are disabled until
  restore;
- Workflow detail keeps one stable set of `Canvas`, `Schedules`, `Run history`,
  and `Settings` tabs. The detail header owns the canonical save action. The
  save action is disabled when the local Builder draft already matches the
  latest saved content. The
  canvas toolbar keeps authoring tools on the left and execution controls on
  the right, including a compact automatic-run status before validation and
  direct Run. A Workflow remains only a reusable definition until a Schedule
  is created and enabled;
- automatic-run status is derived from bounded authoritative Binding reads,
  never from a truncated first page. A Plan with no latest published version
  requires a save first; no non-archived Bindings is not configured; Bindings
  with no enabled item are off; and any enabled Binding is on. Loading and read failure
  remain distinct neutral states. The Inspector explains the same state and
  exposes at most one applicable action: create, manage, view, or retry. A
  read-only Workflow does not expose an invalid create action;
- the visual Inspector renders a deterministic summary of the selected entry,
  node, or edge from the current AST. It does not infer Binding, schedule, or
  runtime facts. An ordinary selected Node additionally has a temporary
  component-based edit form over the same AST; it does not persist a second
  form model;
- the Inspector has three explicit tabs. `Workflow information` renders Plan
  provenance, its typed contract, and a bounded related-Trigger summary with a
  sticky `AI edit` action. `Node information` renders the deterministic AST
  summary of the selected canvas member. Ordinary Nodes expose equal `Edit`
  and `AI edit` actions; Entry and Edge remain AI-only. Direct edit uses
  protocol-specific controls for all current v1 Node types, preserves values it
  cannot safely normalize, and applies focused AST mutations only after the
  Node and complete candidate Workflow validate. `Cancel` discards the
  temporary form, while `Apply` updates the local Builder draft; the existing
  Builder save action remains the persistence boundary. `AI edit` renders the
  Plan's continuous Workflow Scope message stream and its composer. The
  Inspector shell remains mounted while switching between visual canvas and
  YAML source so the right pane, active tab, selection context, and composer
  draft do not flash or disappear. A direct Node form follows the discard
  contract below rather than becoming a hidden per-Node draft;
- direct Node edit keeps `id`, `type`, branch/port identity, Join ownership,
  and fan-out WorkItem provenance read-only. Resource-typed values are
  selection-only, references are filtered by the current authoring context,
  and expression text uses insertion assistance rather than JSON/YAML.
  Direct forms focus and show the Node-specific executable choices before the
  canvas name, input contract, or optional policy. Fields that are fixed by the
  current v1 protocol remain in the deterministic summary rather than appearing
  as inert form controls. Conditional settings are hidden until the controlling
  choice makes them applicable, but any configured legacy or invalid value
  remains visible so the Human can repair or remove it. In
  particular, send-message callback settings appear only for callback
  completion or retained callback configuration, and Thread creation appears
  only for a compatible sent destination or a retained configured value.
  Valid legacy and complex values that the direct controls cannot safely
  reproduce remain visible and byte-preserved. An unknown future Node or source
  field keeps the visual document from claiming a safe direct edit; exact
  inspection and repair remain available through YAML or AI edit. A dirty form
  requires one discard confirmation before selecting another graph member,
  opening YAML or AI edit, or leaving the Workflow. If the selected Node
  changes externally, Apply is blocked until the form is reopened; unrelated
  external context changes are revalidated against the latest document. After
  Apply, ordinary route navigation and browser unload warn before discarding
  an unsaved Builder draft;
- an in-app navigation warning for an unsaved Builder draft offers cancel,
  discard and leave, and save and leave. Save and leave invokes the same
  draft-CAS, validation, immutable-version publication, and default-version
  selection operation as the header save; navigation continues only after
  success. Failure keeps the dialog and edits intact. The action is unavailable
  when that canonical save cannot run. A dirty temporary Node form is resolved
  by its own discard boundary first, and browser unload continues to use the
  browser-native warning;
- the AI composer renders its request target as a context attachment above the
  instruction field. With an empty composer, a Human selection or explicit
  edit handoff may replace that target. Once text exists, its attachment stays
  fixed: selecting another canvas member never silently retargets the draft.
  An explicit handoff to a different target asks whether to keep the current
  context or use the selected Node, preserving the text either way. A
  structural `Edit with AI` handoff may seed a localized, editable instruction
  only when the composer is empty; otherwise it offers the instruction for
  explicit insertion. Nothing is sent automatically. Clearing the attachment
  explicitly returns the draft to Plan-level context, which remains sendable.
  Opening AI edit, drafting the instruction, and choosing an Agent do not
  require entering through the Node-information edit action or selecting the
  context first; the Human may select, replace, or clear the canvas context
  afterward. A compact upward-opening preset menu covers the Workflow's
  action, data, decision, loop, parallel, fan-out, wait, approval, and
  success/failure control patterns. Choosing a preset adds its localized request
  to the composer for further editing and never sends it automatically. Its
  composer entry is an icon-only common-instructions control with a localized
  accessible name and tooltip; menu icons align with the first text line rather
  than the full multi-line item. A sent request retains its server-validated
  context anchor
  in existing message metadata even when the later canvas selection changes,
  and the Human transcript renders that retained Entry, Node, or Edge below
  the sent message as an attachment-like context item. The anchor helps the
  Agent understand the request and is not a write boundary for the Draft;
- configuration changes may be delegated only by an explicit Human action to
  one Agent owned by that Human. The selector starts unselected, loads a
  bounded page from the ordinary owned-Agent directory only on open, uses
  ordinary individual-Agent avatar/search and exact-delivery conventions, and
  excludes Humans, Services, broadcasts, Agent Teams, and Agents owned by
  another Human. Request submission revalidates the exact selected Agent and
  its backing-Machine ownership inside the Plan transaction; it does not add
  online, daemon-version, or CLI-capability filters;
- the AI editor never creates or opens a DM, reply thread, session, or hidden
  Channel. Selection changes do not load another transcript or cause a write,
  and sending keeps the Human in the Builder;
- delegation first saves the current source and layout through the ordinary
  complete-validation and draft CAS boundary. The Workflow adapter then writes
  one ordinary message to the Plan's canonical Workflow Scope with
  server-owned request metadata containing the saved draft revision, selected
  responder, and AST context anchor. Workflow Plan management owns Plan,
  selected-responder, active-wake, and reply orchestration; it composes the
  message domain's idempotency, sequence allocation, exact direct-mention
  outbox, projection, and delivery primitives. Compiler diagnostics are
  carried only in the bounded selected-Agent delivery payload; they are not
  copied into the durable request metadata or Human transcript;
- every changed Draft save emits the live-only
  `runtime.workflow_plan_draft_updated` event with `planId`, `revision`, and
  `updatedBy`. It does not carry source or layout; a clean Builder refetches
  only after observing a newer revision, while a locally dirty Builder keeps
  its edits and reports the conflict. Visible-scope registration is a
  listen-before-refetch barrier: initial open and reconnect refetch the exact
  mounted Workflow message page and reconcile the saved Draft, so missing a
  live-only event cannot leave the canvas stale or preserve a stale active-wake
  footer;
- at most one Agent-edit wake for a Plan may be non-terminal. While it is
  active, direct edit, another AI request, and Human publish controls remain
  mounted but disabled, and the server rejects a second request under the Plan
  transaction lock. The same selected responder is also forbidden from using
  Agent Plan `publish`, `submit`, or `archive` for that Plan until the wake is
  terminal; this server guard is independent of prompt wording and UI state.
  Opening or restoring the Plan reads the latest durable wake through a
  bounded status-only endpoint, so this lock does not depend on first opening
  the AI tab or loading message history.
  Selector changes never interrupt an Agent; terminal `reply_sent`,
  `completed_without_visible_reply`, or `failed` status re-enables delegation;
- exactly one visible Agent reply may follow a Human edit request. Reply
  idempotency replay is checked first; the Plan-locked reply transaction then
  rejects a second distinct reply even before the asynchronous
  `visible_reply_sent` runtime receipt arrives. The accepted reply records
  terminal `reply_sent`, so it is the Agent's final user-facing reply rather
  than a progress acknowledgement. Draft revision and `updatedBy`, not reply
  text, remain the durable evidence that an edit succeeded;
- the selected Agent receives only the current request, its base Draft
  revision, the context anchor, and diagnostics; the request does not copy a
  source snapshot into message metadata or delivery. The Agent loads that
  exact Plan's current saved source through the existing `workflow plan show`
  action, validates as needed, and performs at most one changed `update` using
  that exact base revision. While the matching wake is active, the Agent API
  rejects an `update` from that responder whose expected revision is not the
  request baseline. A stale or repeated write with the baseline fails the
  ordinary Draft CAS after the first changed save, so the Agent cannot rebase a
  second update within the same request.
  Workflow Scope
  history and search remain unavailable to Agents, including Agents with
  retained Scope membership. The Agent may update the whole Draft through the
  existing authenticated Workflow Plan CLI and replies through the ordinary
  trusted current-target message path into the same Workflow Scope;
- after that request's wake becomes terminal, the Builder forces one Draft
  reconciliation. A request-local Review is attributable only when the final
  Draft revision is exactly the base revision plus one, `updatedBy` is the
  selected responder Agent, and source or normalized layout changed. Any
  additional revision or different updater exits request-scoped Review and
  undo rather than presenting an approximate AI diff. A failed wake may still
  open Review when its one changed Draft is attributable. If the terminal
  request left the confirmed Draft revision unchanged, the same AI panel
  reports that no Workflow change was made and does not open Review;
- attributable AI changes appear as a compact Review card between the existing
  message timeline and composer; neither the conversation nor canvas is
  replaced. The existing canvas shows restrained markers for added or changed
  members and a stronger temporary focus when a Review row is hovered or
  selected. The summary groups graph members, Workflow configuration, layout,
  and otherwise-unattributable source changes without creating a comparison
  canvas or general diff language. `Finish review` keeps the already-saved
  final Draft. `Undo this change` writes the captured baseline through the
  ordinary Draft CAS only while the reviewed final revision is still current;
  conflict reloads the latest Draft and never overwrites it. Review state and
  one-click undo are session-only and are not reconstructed from timers,
  replies, or browser storage;
- Human history is cursor-paged only after opening AI edit and requires both
  current Plan authoring authority and the Workflow product read gate.
  Membership alone never grants Workflow transcript access. The Workflow Scope
  is excluded from Inbox, ordinary conversation lists, ordinary search, unread
  badges, scope pickers, the ordinary message-delivery diagnostics surface, and
  the `loop.plan/v1` send-message scope union;
- archived Plans keep the deterministic information and existing AI-edit
  history readable, but hide or disable all edit controls. An Agent save that
  arrives after archive is rejected at the Draft boundary. Archiving while a
  request is active immediately shows a warning toast; the Builder also keeps
  the ordinary wake failure presentation and does not interrupt the
  already-running Agent;
- v4 authoring exposes selected-responder mentions, WorkItems, `fan_out`, its
  concurrency policy, and `join.fanOutId`, while omitting nonexistent controls
  such as an author-supplied `maxItems`;
- the UI exposes only node/gateway behavior implemented by the compiler and
  runtime. It does not retain controls for rejected syntax or absent features.

The server draft, not browser local storage, is the durable source of unfinished
work. YAML and visual saves use the same draft CAS boundary.

Run details render each dynamic fan-out activation as one bounded group rather
than one visual graph node per WorkItem. The group shows terminal/running/ready
counters, source-ordered lazy item pages, `dependsOn`, `blockedBy`, responder,
dependency results, and work-scope links. Canonical Task scopes may be resolved
on click; the UI must not prefetch one Task detail request per item. Dynamic
item activations and effects are folded into the same group in the graph, while
repeated activations of the same static `fan_out` node remain separate by
runtime scope/owner activation identity.

## 7. Later-object contracts

The following ownership rules are normative even when implementation proceeds
one object at a time. Plan APIs and Plan source must not absorb these concerns.

### 7.0 Authenticated Agent Workflow CLI contract

Agent management of the model-visible Workflow lifecycle is owned exclusively
by the hidden Exec CLI surface. It is not an MCP surface and does not register
`workflow_plan`, `workflow_binding`, `workflow_invocation`, `workflow_run`, or
`workflow_set_variable` tools. The only model-visible entry points are:

```text
loop exec workflow plan    '<object|->'
loop exec workflow trigger '<object|->'
loop exec workflow run     '<object|->'
loop exec workflow step    '<object|->'
```

Each command represents one first-class object rather than one tool per
operation. Its root input is exactly one JSON object whose `action` selects a
closed, canonical snake-case union:

| Object command | Closed action union                                                            |
| -------------- | ------------------------------------------------------------------------------ |
| `plan`         | `create`, `list`, `show`, `validate`, `update`, `publish`, `submit`, `archive` |
| `trigger`      | `create`, `list`, `show`, `validate`, `update`, `enable`, `disable`, `archive` |
| `run`          | `start`, `list`, `show`, `logs`, `cancel`                                      |
| `step`         | `list`, `get`, `complete`, `fail`                                              |

Plan `create` 和 `list` 接受可选且可信的 `teamId`。带 Team ID 的 `create` 会同时
创建持久 Team Workflow 关系与 Plan，仅允许该 Team 的当前 Leader Agent 调用；
当未删除关系已存在时必须失败。带 Team ID 的 `list` 是 Team discovery，而不是
workspace filter：存在已审批且可执行的 Plan 时返回该 Plan；新 revision 等待审批
期间，仍返回最后一个已审批版本。省略 `teamId` 时保留普通未关联 Plan 的语义。

`submit` is the only publication entry point for a Team-linked
Plan. It requires `teamId`, `planId`, and positive `expectedRevision`;
`makeDefault` is optional. The response identifies the Team Workflow relation,
published PlanVersion, relation revision, and immutable Plan/draft history.
Team-linked direct `publish` requests are forbidden even when the caller knows
the Plan ID. Dynamic one-off Plans omit `teamId` and may use the ordinary
validate and publish lifecycle.

Agent-assisted Builder editing uses only the Plan `show`, `validate`,
and `update` actions. `update` preserves the stored visual layout byte-for-byte
when `layout` is omitted. It creates a new revision only for changed source or
layout that passes complete validation and the expected-revision CAS. Invalid,
no-op, stale, or archived updates do not create a revision. These actions
mutate only the mutable Draft; they do not publish a version or create or
modify a Trigger, Run, or Binding.

The final argument accepts either one inline object or `-` for stdin, with a 1 MiB
CLI input limit; stdin is read through a bounded reader rather than buffered
without limit. The ordinary shell path fails closed before reading stdin when
there is no valid one-shot Exec launch authorization.

An action with leading or trailing whitespace, a hyphen alias for a snake-case
action, an unknown field, a duplicate JSON key at any depth, excessive JSON
nesting, or a second root value is invalid. The daemon validates this union
before authorization, removes transport-owned fields from the business input,
and re-inserts the selected canonical action when constructing the server
request. Thus a model cannot replace the action after dispatch.

Authenticated object-management requests use exactly one canonical Agent
Runtime endpoint per object:

```text
POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workflow/plan
POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workflow/trigger
POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workflow/run
POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workflow/step
```

The daemon, not the model, wraps the selected input in this strict envelope:

```json
{
  "context": {
    "agentId": "agt_...",
    "workspaceId": "wsp_...",
    "commandId": "cli:0123456789abcdef0123456789abcdef"
  },
  "input": {
    "action": "...",
    "...": "action-specific business fields only"
  }
}
```

`commandId` is newly generated by the daemon for each logical command from 16
bytes of cryptographic entropy. Its only valid representation is `cli:` plus
exactly 32 lowercase hexadecimal characters. The server authenticates the
Agent Runtime request, requires the URL, authenticated principal, and envelope
Agent/workspace identities to match exactly, and rejects provider tool-call
identities, transport request numbers, caller-authored owners or principals,
and unknown context fields.

The daemon supplies an `Idempotency-Key` for every public mutation with a
replay-safe transport: Plan `create`, `update`, `archive`, and `publish`; Trigger
`create`, `update`, `enable`, `disable`, and `archive`; Run `start` and `cancel`;
and Step `complete`, `fail`, and `pause`. Step `park` is intentionally omitted:
its optional legacy key is accepted for compatibility but is not a durable
replay identity. The daemon derives, and the server constant-time verifies, the
exact key:

```text
"workflow-cli:" + lowercase_hex(
  SHA-256(
    agentID + "\x00" + workspaceID + "\x00" + object + "\x00" +
    action + "\x00" + commandID
  )
)
```

The SHA-256 digest is not truncated. After one ambiguous network or `5xx`
failure, the daemon may transparently retry a receipt-backed POST once. That
retry must reuse the exact same envelope bytes, `commandId`, and derived
`Idempotency-Key`. Read-only actions are not transparently retried and do not invent a header.
Mutation retries reuse the exact same command identity and idempotency key; an
ambiguous outcome is surfaced when the transport cannot prove the committed
receipt.

Step callback actions `list`, `get`, `complete`, `fail`, `pause`, and `park` use the
model-visible `loop exec workflow step` action union. Every action explicitly
selects a Run; `get`, `complete`, and `fail` also select one Step. These IDs are
resource locators, not credentials. Workspace and Agent identity come from the
authenticated route principal, while the callback service reloads the durable
Run, activation, effect request, and delivery receipt and verifies that the
frozen callback responder equals the authenticated Agent and member pair. It then
enforces the current attempt, waiting state, output/work-scope/WorkItem contracts,
and idempotency semantics (including replay/conflict for pause; park's repeatable
FSM transition has no lifecycle-wide replay receipt) in the same first-result-wins
transaction used by the runtime. The daemon's active delivery snapshot may provide UI guidance and be
cleaned up after a matching terminal result, but its absence never blocks an
authorized Step operation.

Run management actions use `/workflow/run`; Step actions enter the separately
fenced callback runtime boundary at the authenticated Agent route
`POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workflow/step`.
`list` accepts a Run ID and returns only awaiting Step selectors whose frozen
responder is the current Agent. The Server stores waiting state durably; it does
not retain an open request or network connection while waiting for completion.

For a v3 or v4 Agent collaboration step, `get` returns the collaboration
context for the explicitly selected Workflow callback:

```json
{
  "deliveryScope": {
    "kind": "task",
    "scopeId": "tsk_dispatch"
  },
  "workScope": null,
  "relatedScopes": [
    {
      "kind": "thread",
      "scopeId": "thr_requirements"
    }
  ],
  "teamMemberContext": {
    "teamId": "team_wf",
    "teamHandle": "wf-team",
    "teamDisplayName": "WF Team",
    "teamPurpose": "Ship the release.",
    "teamMemberId": "wtm_responder",
    "role": "member",
    "roleLabel": "Builder",
    "roleOverlay": "Build delegated work."
  }
}
```

`teamMemberContext` is present for any Run with a frozen Team ID (linked or
dynamic-explicit) whose responder is a frozen, valid Team member. It is
resolved and frozen server-side at step delivery and never contains the team
roster, other members' duties, wake configuration, or Team
division hints. It renders through the dedicated Workflow-specific hidden
context path, never through the ordinary `@Team` formatter.

These fields are trusted callback context rather than business-message text.
When `workScope` is `null`, successful completion adds the final binding beside
the declared business output:

```json
{
  "action": "complete",
  "workScope": {
    "kind": "task",
    "scopeId": "tsk_implementation"
  },
  "output": {
    "summary": "implemented"
  },
  "idempotencyKey": "..."
}
```

When `workScope` is already concrete, `complete` may omit it or repeat exactly
that value. Legacy v1/v2 callback attempts omit `workScope` and
`relatedScopes` from `get` and retain their published callback
contract. All scope validation and completion rules are described in §4.5.

For a v4 Planner step, `get` additionally returns the frozen WorkItem
contract:

```json
{
  "workItemsContract": {
    "required": true,
    "maxItems": 256,
    "maxKeyBytes": 256,
    "maxRelatedScopesPerItem": 64,
    "maxDependencyEdges": 2048,
    "maxRelatedScopeReferences": 512,
    "maxJsonValues": 6144,
    "maxJsonBytes": 1048576,
    "allowedResponders": ["agt_dev_one", "agt_dev_two"],
    "dependencyValueContract": [
      {
        "name": "result",
        "schema": {
          "type": "number",
          "required": true,
          "default": null,
          "values": [],
          "items": null
        }
      }
    ]
  }
}
```

The Planner completes the same exact callback with business output and the
closed WorkItem array kept separate:

```json
{
  "action": "complete",
  "output": {
    "plan_summary": "seven arithmetic operations"
  },
  "workItems": [
    {
      "key": "sum_left",
      "dependsOn": [],
      "workScope": { "kind": "task", "scopeId": "tsk_sum_left" },
      "relatedScopes": [{ "kind": "task", "scopeId": "tsk_expression" }],
      "responder": "agt_dev_one",
      "instruction": "Compute 17 + 25."
    }
  ],
  "idempotencyKey": "..."
}
```

The returned contract is self-describing: its item, key, per-item scope,
aggregate dependency/scope, canonical value, and canonical byte limits are the
same frozen limits enforced by both daemon and server.
`dependencyValueContract` is the exact compiled contract for the object stored
at every direct dependency envelope's `value`; a Planner must choose
`$dependency.path` relative to that contract (for example, `$op08.result` for
the contract above). Before the Planner callback commits, the trusted server
uses that frozen contract to reject every statically decidable reference
error: the first path segment must name a declared field; `number` and
`boolean` fields are valid only as one-segment paths; declared `string`,
`enum`, `member`, `agent`, `channel`, `scope`, `duration`, `datetime`, `array`,
and `work_items` leaves are not materializable; and an `object` field must be
followed by at least one internal segment. ArtifactContract does not describe
object properties, so paths below an object remain runtime checks. A
`required: false` field is accepted when its path and declared type are
statically valid; if a particular dependency omitted it, the runtime resolver
fails that WorkItem before delivery. The runtime resolver remains the final
defense for actual path presence and the `number`/`boolean`/`null` leaf rule.

The daemon performs the same closed-shape and reference-syntax checks before
sending the request, and the callback service repeats canonicalization,
contract-aware reference validation, DAG validation, responder allowlisting,
scope authorization, final durable-payload budget validation, and
first-result-wins replay checks at the trusted server boundary. An omitted
`workItems` field is invalid when the contract says it is required; a present
empty array is a valid zero-item Plan. Legacy v1-v3 callbacks do not expose
`workItemsContract` and reject a supplied `workItems` field.

The former Workflow MCP registrations, the standalone `workflow set-variable`
Exec command, `toolCallId`-derived or `mcp:` idempotency keys, and the
per-operation `/workflow-bindings/tools/*` and
`/workflow-invocations/tools/*` routes are removed compatibility surfaces, not
aliases. Requests to the removed HTTP paths return `404`.

### 7.1 Binding ownership

Binding owns only durable automatic schedule deployment:

- one immutable Plan reference and append-only Binding revisions;
- exactly one `schedule` activation source per revision, including its
  normalized occurrence input;
- one exact pinned PlanVersion ID per revision;
- lifecycle state `disabled`, `enabled`, or `archived`;
- canonical execution owner/principal and separate creation provenance.

The stable Binding aggregate owns its ID, workspace, Plan, display metadata,
execution owner, lifecycle revision, and active BindingRevision pointer. Its
Plan reference and execution owner do not change. Editing executable deployment
configuration appends a BindingRevision; it never mutates a historical revision
in place. A revision contains the exact PlanVersion ID, one normalized schedule
source, semantic digest, authoring provenance, and creation time. Binding has no
manual source, invocation form, allowed-role list, input-default layer, or
constant-override layer.

Binding management provides a complete lifecycle without hard deletion:

- create the Binding and revision 1 atomically, with an explicit choice whether
  to activate it;
- list and read Bindings, and update display metadata under a Binding ETag;
- validate proposed revision configuration without persistence;
- append, list, and read immutable revisions;
- activate one exact revision, disable the Binding, archive it, and restore it;
- restore always returns the Binding to `disabled`; activation is a separate,
  explicit command.

Activation creates a new mutable source reservation for the selected immutable
revision and atomically supersedes the previous reservation. Disabling or
archiving closes the current reservation but does not cancel an Invocation or
Run already accepted. There is no `DELETE` route or delete-shaped archive.

A human-created Binding uses that human as execution owner. An Agent-created
Binding records the Agent as `createdBy` provenance but resolves the Agent's
trusted managing/owner human as the canonical execution owner. No caller may
supply an arbitrary owner, workspace, creator, or message author. Owner and
creator are distinct: Agent provenance never grants runtime authority, and an
Agent with no unique active managing human cannot create a Binding.

Only that immutable execution owner may update Binding metadata, append or
activate a revision, disable, archive, or restore the Binding. Other workspace
members may retain the existing read access, but the Web UI does not expose an
edit entry or lifecycle actions to them, and a direct edit URL returns them to
the read-only detail. The service-side exact owner check is authoritative for
every mutation.

Human Web/API management and Agent Workflow CLI management call the same
Binding service and enforce the same validation, CAS, authorization, and
idempotency rules. The Agent surface is the single `loop exec workflow trigger`
object command defined in §7.0, with a closed `action` union rather than one
model-visible command per lifecycle operation. Its adapter obtains Agent
identity, workspace, managing-human relation, and command identity from the
authenticated runtime and daemon-authored envelope; these are not
model-authored parameters. Idempotency binds the authenticated Agent,
workspace, `trigger` object, canonical action, and logical CLI command ID, so
retries of one command recover the durable committed result while separate
commands cannot collide through a reused transport request number.
Revision append, activation, metadata, and lifecycle commands persist their
original response snapshot; create preserves the same Binding and immutable
initial revision/source identities even if the aggregate is read again after
later lifecycle changes. The CLI is a thin adapter over the same contract
rather than another implementation.

Plan archive does not stand in for Binding deactivation. Existing Binding
revisions retain their immutable PlanVersion snapshot and are not silently
disabled. Creating a Binding or appending a revision from an archived Plan is
rejected until the Plan is restored.

#### Binding schedule dispatch

Binding revisions support only the `schedule` source. `manual`, Task activity,
webhook, message, and other event source kinds are invalid rather than aliases
or compatibility modes. Source configuration maps the schedule occurrence's
input into the Plan input contract; it does not add `event.*` or trigger fields
to Plan source.

Revision validation first resolves the exact PlanVersion from the same
workspace and Plan, verifies that its compiled artifact and semantic digest are
intact, and loads the versioned Plan input contract. The server then normalizes
the schedule and validates its occurrence input. Unknown input names and
invalid typed values are rejected. Accepted scheduled input has exactly this
precedence, from lowest to highest:

```text
Plan input defaults
  < schedule occurrence input
```

The result must satisfy every required Plan input and its declared type before
an Invocation is created.

A schedule source supports exactly one time mode: absolute one-shot time,
relative one-shot delay, fixed interval, or five-field cron with an IANA
timezone. Relative time is materialized to an absolute occurrence when the
revision is activated; intervals have an explicit anchor. Invalid, ambiguous,
or multiply specified modes are rejected.

The mutable source reservation stores the next due occurrence independently of
the immutable revision. The dispatcher carries the workspace placement token
from due-candidate selection into one reservation transaction. That transaction
fences placement, locks the source, reserves a unique occurrence, asks the
Invocation boundary to accept it, and advances the cursor. Occurrence identity
is stable across retries; retrying the same occurrence returns the existing
Invocation. Recurring schedules preserve every due occurrence rather than
silently skipping missed cadence, while each worker pass processes a bounded
number so one Binding cannot monopolize dispatch. A one-shot source closes only
after its occurrence has been durably accepted.

Binding activation materializes only the initial durable reservation. There is
deliberately no standalone schedule lease or cursor-advance action: occurrence
acceptance and cursor movement are implemented with Invocation as the same
transactional boundary, so a crash cannot advance past an Invocation that was
never durably accepted. Authored and materialized schedule instants use UTC
`DATETIME(6)` precision and range before revision digesting or persistence.

### 7.2 Invocation ownership

Invocation owns one accepted occurrence from either the direct Plan path or the
scheduled Binding path.

A human direct request uses:

```text
POST /api/v1/workspaces/:workspaceId/workflow/plans/:planId/invocations
Idempotency-Key: <bounded caller key>

{"input": {...}, "planVersionId": "wpv_..."}  // planVersionId is optional
```

The equivalent Agent request is the `loop exec workflow run` `start` action
defined in §7.0. Both are Plan operations: omitting `planVersionId`
selects the Plan's default immutable version, while providing it selects that
exact version. Neither path looks up, creates, or synthesizes a Binding.

Direct input is assembled with exactly this precedence:

```text
Plan input defaults
  < request input
```

The result is normalized and must satisfy the selected PlanVersion's complete
input contract. For a human request, the authenticated current workspace
principal supplies the execution owner, principal member, visible author, and
human `invokedBy` provenance. For an Agent request, `invokedBy` is the
authenticated Agent, while the unique current managing human and that human's
active workspace membership supply the execution owner, principal member, and
visible author. A caller cannot supply or override any of these identities.

A direct Invocation has source kind `manual` but no manual Binding. Its entire
Binding snapshot tuple is `NULL`:

```text
(binding_id, binding_revision_id, binding_revision,
 binding_source_id, binding_activation) = (NULL, NULL, NULL, NULL, NULL)
```

A scheduled Invocation is accepted only from an enabled schedule reservation.
It copies the BindingRevision's exact pinned PlanVersion, the normalized
scheduled input, and the Binding owner's trusted execution identity. Its
Binding snapshot tuple is complete: Binding ID, BindingRevision ID and number,
Binding source ID, and positive activation are all present. Schedule occurrence
identity is provenance; the Binding owner remains execution owner, principal,
and visible message author.

Invocation acceptance atomically:

- reserves the direct request idempotency identity or stable schedule
  occurrence identity;
- resolves and locks one exact PlanVersion;
- validates and snapshots final Plan input and source provenance;
- snapshots the trusted execution identity and, only for schedule, the complete
  Binding tuple;
- creates exactly one corresponding queued Run and its first append-only event.

The direct request digest binds workspace, Plan, canonical request input,
authenticated actor, and an explicit selector: `default` when
`planVersionId` is omitted or `version:<planVersionId>` when it is present.
Those selectors remain distinct even when default currently points at that
same exact version. Reusing one actor-scoped idempotency key with a different
Plan, selector, input, or actor is a conflict.

Exact direct replay is checked before mutable Plan/default, human membership,
or Agent-manager reads. It returns the original Invocation, exact PlanVersion,
identity, and Run even if those mutable relationships later change. Schedule
replay is keyed by its stable occurrence and likewise returns the original
immutable receipt.

### 7.3 Run ownership

Run owns execution state:

- one immutable Invocation reference and matching Plan and exact PlanVersion
  references;
- the Invocation's trusted execution-identity and typed-input snapshots,
  versioned Run state, and terminal Plan output/error;
- graph tokens and their fork/fan-out/loop lineage;
- node activations and retry attempts, each with immutable input/output;
- dynamic fan-out scopes with their frozen source-ordered WorkItems, semantic
  IDs, DAG dependencies, concurrency policy, item lineage/status/result, and
  one closing join activation;
- waits, approvals, timers, callbacks, and leases;
- effects and append-only sequence-ordered Run events;
- idempotent stop/cancel and terminal status.

Run has no Binding ID, BindingRevision ID/number, Binding source ID, or Binding
activation field. Scheduled Binding provenance remains available through the
immutable Invocation; it is not denormalized back into Run. Direct and
scheduled Runs therefore share one execution shape.

Run-history collections are ordered by `startedAt` descending with Run ID
descending as the stable tie-breaker. Runs that have not started sort after all
started Runs. Pagination cursors preserve this ordering tuple so clients can
load history incrementally without sorting individual pages.

The execution model is token/activation/attempt based, not a single mutable
`currentStep`. Runtime callback and timer correlation always names an exact
attempt. A dynamic WorkItem owns an entire static activation lineage, so its
concurrency slot is released only when that lineage reaches its join or a
terminal item boundary. Stop/cancel prevents new activations, closes waits and
callback capabilities, settles active fan-out lineages as canceled, and is
idempotent. Late callbacks cannot revive a canceled or terminal item or Run.

### 7.3.1 Run Team and member context

A Run's frozen Team ID comes from exactly one server-authorized source:

- **Team-linked Plan**: a Plan with an approved, activated (`activated_at`
  set by the direct-publication lifecycle) and non-deleted `workspace_team_workflows`
  relation freezes that relation's Team ID at Invocation acceptance. A
  bound-but-never-approved, rejected, or deleted relation never yields a Team
  ID.
- **Explicit Team ID for unlinked/dynamic Runs**: `invoke` accepts an optional
  `teamId`. For an unlinked/dynamic Plan, the server authorizes the caller
  (through its invoking human/Agent member) as a valid, active member of that
  Team before freezing it. A Team-linked Plan keeps its authoritative relation
  Team; a conflicting explicit `teamId` is rejected.
- **Dynamic without a Team Context**: no `teamId` and no relation leaves the
  frozen Team ID empty; such Runs behave exactly as ordinary non-Team Runs.
  There is no automatic Team inheritance — an invoking Agent that wants a
  dynamic Run to carry a Team must pass an explicit `teamId`.

The Team ID is never derived from visible message text or a callback request,
so it cannot be forged.

The frozen Team ID is carried into every `send_message` effect request of the
Run. Before any message is persisted or fanned out, the ordinary message
transaction authorizes the resolved callback responder as an active member of
the frozen Run Team; a rejected responder rolls back the whole write with zero
side effects (no message row, no fan-out, no wake, no unread/notification). The
frozen **minimal member context** of the responder is atomically persisted into
the durable message metadata/outbox receipt at that same transaction, so
callback, retry, recovery, replay, and fan-out reuse the exact first-frozen
snapshot and never re-read mutable Team/member/role state. The Step `get`
receipt exposes that same frozen context verbatim.

The frozen member context is strictly per-responder and contains only:

- minimal Team identity and purpose (Team ID, handle, display name,
  description);
- the responder's stable `teamMemberId`, Team role, role label, and role
  overlay/duty.

The delivered recipient is decorated with a Workflow-specific hidden Team
context carrying exactly these fields and **no roster**, so the daemon prompt
surfaces the frozen context rather than reusing the ordinary `@Team` full
roster. It never includes the team roster, any other member's duties, wake
configuration, or Team division hints. Resolving fails closed: an inactive
Team, an unknown/archived responder Agent, or a responder who is not a current
Team member rejects the step delivery rather than delivering under a forged
membership.

### 7.4 Path and snapshot invariants

The two acceptance paths have deliberately different source snapshots and the
same Run shape:

| Path         | Version selector                            | Invocation Binding tuple | Execution identity                                                      |
| ------------ | ------------------------------------------- | ------------------------ | ----------------------------------------------------------------------- |
| Direct human | Plan default or exact PlanVersion           | all `NULL`               | authenticated current human principal                                   |
| Direct Agent | Plan default or exact PlanVersion           | all `NULL`               | authenticated Agent provenance; unique current managing-human principal |
| Schedule     | exact PlanVersion pinned by BindingRevision | complete and non-null    | snapshotted Binding owner                                               |

A partially populated Binding tuple is invalid. Source kind `manual` requires
the all-`NULL` tuple and a complete authenticated human/Agent `invokedBy` actor;
source kind `schedule` requires the complete tuple and schedule occurrence
provenance. Every accepted Invocation names exactly one immutable PlanVersion,
and exactly one Run names that Invocation, Plan, and PlanVersion. The Run copies
only the accepted input and execution-identity snapshots needed for execution;
it never acquires Binding columns based on source kind.

## 8. Durable execution invariants

Plan, Binding, Invocation, and Run implementations preserve these cross-object
invariants:

1. Every Invocation is locked to one exact PlanVersion. Only a scheduled
   Invocation is additionally locked to one immutable BindingRevision; a direct
   Invocation has the all-`NULL` Binding tuple. Run references the Invocation,
   Plan, and PlanVersion and has no Binding fields.
2. Default/exact selector resolution, final input validation, trusted identity
   snapshotting, Invocation idempotency, and queued-Run creation occur in one
   consistency boundary.
3. Runtime candidates carry the workspace placement `(group, state, epoch)`
   that made them eligible; transactions that create or advance work lock and
   revalidate that placement.
4. Run events are append-only and sequence-ordered per Run.
5. External effects reserve stable idempotency keys and persist durable effect
   references. Transport retries do not duplicate effects. A callback effect's
   selected responder, when present, is rendered as a canonical visible mention
   through ordinary routing; the Agent frozen by the accepted callback receipt
   cannot be re-inferred or retargeted on replay.
6. The visible author of every `send_message` attempt is the Invocation's
   snapshotted author member: the trusted human/current Agent manager for a
   direct request or the Binding owner for a schedule. Effect time revalidates
   active workspace membership and current target permission; loss of authority
   fails closed without switching identities.
7. Waits and their optional deadlines, approvals, and retry backoff are
   persisted reservations; callback delivery waits are private runtime
   reservations. Workers do not sleep while waiting.
8. Each node visit and retry has exact activation/attempt identity. Fork,
   fan-out item, and loop lineage prevents joins, callbacks, timers, and retries
   from correlating with another branch, WorkItem, or iteration.
9. Run-state mutation is typed, audited, and CAS-protected. Parallel conflicts
   rejected by the compiler cannot be reintroduced by runtime arrival order. A
   state write that satisfies persisted waits records the mutation and reserves
   their resumed work in the same transaction.
10. Cancellation is idempotent. It stops new graph work and invalidates late
    completion, while acknowledging that an already accepted external effect
    may not be retractable.
11. Dynamic WorkItems are accepted as one closed, authorized DAG and frozen
    before scheduling. Recovery revalidates persisted semantic IDs, source
    order, dependency results, status transitions, active lineage count, and
    paired join identity before it can expose more work.

These invariants belong to their named later objects. They are not fields in
Plan source and are not reasons to put activation, execution owner, or runtime
ledger data into Plan storage.

## 9. Compatibility and cutover boundary

The current contract provides the intended capability set without syntax or
data compatibility. The parser, API, storage, Builder AST, compiled IR, and
object identities accept only the Plan/Binding/Invocation/Run model.

In particular, the current path rejects:

- `apiVersion: loop.workflow/v1` and `kind: Workflow`;
- trigger blocks inside executable Plan source;
- author-declared `inputs.*`, `event.*`, `state.*`, `steps.*`, and `ctx.*`
  envelope paths from the old document shape;
- inline `next`, `default`, `onApprove`, `onReject`, and `onTimeout`
  transitions;
- a magic `end` string, first-list-element entry, and DAG-only assumptions;
- Binding `manual` sources, Binding input defaults/constants, invocation forms,
  allowed-role lists, and their former request or persistence fields;
- a direct-invocation `bindingId` or any synthesized Binding/BindingRevision;
- Binding IDs, revisions, sources, or activations on a Run;
- legacy AST, compiled payload, object ID, and persisted row shapes.

There is no legacy import, field alias, object-data migration, dual-read,
dual-write, or fallback parser. Mature expression, graph-analysis,
message-effect, and Builder mechanisms are shared only when they implement
this contract; their former wire shape is not a compatibility surface.

The product cutover is complete. Runtime and fresh-schema ownership belongs
only to the Plan, Binding, Invocation, and Run paths described here. The old
Workflow routes, UI, workers, task-event activation, document model, runtime
ledger, and storage tables are not shipped as a fallback. Historical database
migrations that once created those tables remain immutable upgrade history;
one forward cleanup migration drops their output on upgraded databases without
translating it into current objects.

## 10. Conformance and testing

Regression coverage must prove at least:

- Plan create/list/read/update and workspace authorization;
- durable server draft save/load, invalid-draft rejection, diagnostics, no-op
  behavior, and CAS conflict behavior;
- atomic Plan, canonical Workflow Scope, initial Draft, and creator-membership
  creation plus bounded in-place backfill for existing Plans;
- Workflow Inspector deterministic summaries, stable AST context anchors,
  exhaustive v1 Node direct-edit coverage, focused AST mutations, preserved
  unsupported values, dirty/external-update guards, Agent-only lazy selection,
  continuous cursor-paged history, and absence of DM/thread/session fallback;
- one active Agent-edit wake per Plan, exact selected-Agent delivery, request
  idempotency, non-empty composer target preservation, terminal wake recovery,
  single-revision Review attribution, CAS-safe undo, active-wake Agent
  lifecycle rejection, and non-destructive Draft realtime convergence;
- Human product-gated Workflow transcript reads and explicit denial of Agent
  generic history/search, ordinary Inbox/search/unread, and Workflow DSL scope
  targeting;
- Agent Plan `update` complete validation, no-op rejection, archived/CAS
  protection, and preservation of stored layout when omitted;
- atomic publish, immutable source/layout/IR, stable version IDs, and no
  partial writes after compiler failure;
- first-publish current/default initialization, later current-only movement by
  default, explicit `makeDefault`/promotion, and exact-version reads;
- historical republish creating a new current version without mutating the
  source version or draft;
- archive/read/restore behavior and separation from Binding deactivation;
- absence of a Plan deletion action or archive-behind-DELETE behavior;
- 每个 Team 最多一个当前可复用 Team Workflow，并具有稳定的 Team-to-Plan 关系
  ID、由 Team 锁串行化的创建、creator/Team 授权、关系 CAS、命令幂等与审计、
  tombstone 后替换能力，且不会泄漏到其他 Team 的 discovery；
- Team creation through the ordinary Team Channel and fixed Leader mention,
  including preserved composer drafts, no automatic send, and no authoring
  Agent picker;
- Team-linked Plan creation without publication, exact-draft
  Team Leader direct update, Team Leader authorization, and
  atomic publication of only the frozen reviewed source/layout/artifact;
- Team Workflow lifecycle projection for generating, unpublished, awaiting
  approval, changes requested, failed/retry, active, pending update, archived,
  and deleted states, including rejection preserving the draft and a pending
  update retaining its prior approved executable version;
- 取消过期 Team Leader ownership、每个关系最多一个 historical proposal，且每个 proposal 都
  关联到该 Team 唯一稳定的 Team Workflow 关系；
- direct Human and Agent Plan publication guards proving that Team-linked
  `publish`, `republish`, and `make_default` cannot bypass Team Leader ownership;
- Team Workflow deletion atomically cancelling historical proposal, archiving the
  Plan, tombstoning the relation, rejecting late approval, and preserving
  immutable versions and runtime references;
- Team Leader choice among direct discussion, an approved reusable Team
  Workflow, and built-in dynamic one-off orchestration, with only approved
  executable versions returned by Team-scoped Plan discovery;
- Team settings Workflow 区块展示单个成员卡片宽度的 Workflow 卡片或创建空态，使用
  统一生命周期状态，支持返回后重载与 realtime 协调、Team-to-Workflow 导航，且不
  提供独立 tab、反向关联选择器或 dynamic-Workflow 配置界面；
- Binding create/list/read/metadata update, validation, append-only revision
  history, exact PlanVersion pinning, activation, disable, archive, and
  restore-to-disabled behavior;
- strict schedule-only Binding source validation and rejection of `manual`,
  Task activity, other undeclared source kinds, obsolete `inputPolicy`,
  defaults/constants, form, and allowed-role fields;
- scheduled input precedence of Plan defaults below schedule occurrence input,
  plus complete typed-input validation;
- direct human and Agent invocation from a Plan with default-version selection
  on omission, exact-version selection on presence, strict rejection of
  empty/null `planVersionId`, and no Binding lookup or synthesis;
- direct input precedence of Plan defaults below request input, including
  complete typed-input validation and presence-sensitive values;
- direct idempotency digests that distinguish `default` from
  `version:<planVersionId>`, bind canonical request input and authenticated
  actor, reject key reuse with a different logical request, and replay before
  mutable Plan/default/principal/manager reads;
- trusted direct identities: the authenticated current human principal or the
  authenticated Agent's unique current managing-human principal, with rejection
  of caller-authored owner/principal/author/invoker fields;
- all-`NULL` Invocation Binding tuples for direct source kind `manual`, complete
  Binding tuples for source kind `schedule`, and rejection of every partial or
  source-mismatched tuple;
- scheduled author/execution identity remaining the snapshotted Binding owner;
- Run persistence, domain, and APIs containing only Invocation/Plan/PlanVersion,
  input, and identity snapshots—never Binding fields—on both source paths;
- canonical `loop exec workflow plan|trigger|run|step` management with
  trusted daemon command context, exact `workflow-cli:` idempotency derivation,
  equivalence with the human/service APIs, and rejection of model-supplied
  authority or transport fields;
- absence of Workflow MCP registrations, the standalone Workflow
  `set-variable` Exec action, and the removed per-operation Agent Runtime
  Binding/Invocation routes;
- schedule mode normalization, occurrence cursor durability, bounded catch-up,
  placement fencing, and occurrence idempotency across dispatcher retries;
- Binding CAS/idempotency conflicts, revision immutability, source supersession,
  and the absence of hard delete;
- strict rejection of legacy headers, fields, transitions, artifacts, and IDs;
- compiler diagnostics and golden source-to-IR fixtures for every supported
  action, port, mapping, switch, parallel/dynamic-fan-out/join mode, counted
  loop, implicit failure route, and End, including v1-v3 canonical-byte
  compatibility and strict v4 artifact decoding;
- type and context checking for direct references, expressions, templates,
  callbacks, and Plan/node input/output;
- compile rejection of dangling or unreachable graph elements, missing
  terminal routes, fork/join scope errors, unowned cycles, and parallel state
  conflicts;
- runtime E2E for sent and callback messages, selected-responder visible
  mentions and frozen callback receipts,
  retry and late callback, wait timeout, waits, approvals, switch routing,
  nested parallel, both fixed-fork join modes, zero/one/many WorkItems,
  source-ordered bounded scheduling, dependency blocking, fan-out join
  recovery, exact counted loops, optional early break, parallel-in-loop
  correlation, explicit and implicit failure termination, and idempotent
  cancellation;
- Invocation idempotency preserving resolved version, source snapshot, execution
  identity, and Run across concurrent default-pointer, principal/manager, or
  Binding changes;
- placement fencing, append-only Run event order, and effect idempotency;
- Builder visual/YAML round-trip, source-linked diagnostics, server draft CAS,
  immutable version views, layout exclusion from semantic digest, and bounded
  Run UI aggregation that does not render one graph node per dynamic WorkItem;
- fresh bootstrap and upgraded-schema regression proving that retired Workflow
  tables, routes, workers, and task-event cursors are absent while all four
  current object paths still execute end to end.
