-
Notifications
You must be signed in to change notification settings - Fork 3
crew step
Status: Implemented; follow-up review findings addressed.
A workflow can use a Crew project as an executable step. The step invokes one of the Crew's public or platform-internal triggers, waits for that trigger run to finish, and saves the Crew's final text response in the calling workflow's current iteration folder. Later workflow steps can read the Crew's persistent project files through the platform's existing read-only attached-folder mechanism.
This feature makes a maintained Crew available as a reusable specialist inside many workflows. It does not turn Crew projects into workflows or add iteration folders to Crew.
An ordinary workflow step belongs to one pipeline. Its inputs, execution, and outputs are scoped to one workflow run. A Crew is a long-lived project that a person and multiple automations can cultivate over time.
A Crew can retain:
- human-curated memory and instructions;
- selected and configured skills;
- persistent files and attached folders;
- a project database and dashboard;
- main-chat conversation history;
- separate persistent trigger-conversation history;
- schedules, triggers, and bot routes.
Use an ordinary step when the work belongs to the current run and should be reproducible from that run's inputs. Use a Crew when the responsibility, knowledge, or human stewardship continues across runs and workflows.
Calling a Crew does not provide a more capable model by itself. Its advantage is the persistent, human-maintained specialist context.
The workflow Builder adds a step with type: "crew". The step identifies an
accessible Crew project and one of that Crew's triggers. The trigger owns its
saved base instruction and conversation destination. A platform-internal
trigger can be created while the step is configured and is scoped to the
calling workflow. The step supplies the workflow-specific instruction and
runtime input.
The Builder must understand that a Crew is a persistent project, not merely an LLM call. Builder guidance and tools must cover Crew identity, memory, skills, files, attached folders, database, dashboard, triggers, schedules, bots, and conversation destinations.
An illustrative plan.json entry is:
{
"type": "crew",
"id": "review-with-rts",
"title": "Review with RTS Crew",
"description": "Ask the maintained RTS reviewer to review this pull request.",
"crew_profile_id": "work",
"crew_project_id": "rts-pr-reviewer",
"trigger_id": "5f16e1fa-7ddd-4ee5-8311-63b34745ad46",
"instruction": "Read your memory and review skills. Review the pull request supplied in the workflow input. Update your dashboard with the outcome. Return a concise result; if the supporting report is large, save it under reports/ and include its Crew-relative path in the response.",
"context_dependencies": [
"prepare-review/pr.json"
],
"context_output": "response.md",
"timeout_seconds": 1800,
"next_step_id": "publish-review"
}The exact schema should use stable project and trigger IDs. Names, icons, and paths are display metadata and must not be execution identities.
| Field | Purpose |
|---|---|
id |
Stable workflow step identity. |
title |
Human-readable node title. |
crew_profile_id |
Product profile; initially work. |
crew_project_id |
Stable target Crew project ID. |
trigger_id |
Existing trigger to invoke. |
instruction |
Workflow-specific instruction rendered at run time. |
| Field | Purpose |
|---|---|
context_dependencies |
Workflow outputs made available as trigger input. |
context_output |
Response filename; defaults to response.md. |
timeout_seconds |
Maximum wait for the Crew run. |
next_step_id |
Explicit successor using normal workflow navigation. |
The selected Crew trigger has one of the existing run destinations.
The invocation continues in the same persistent conversation used by the person and the Crew. Choose it when the instruction depends on discussion in that main conversation.
The invocation continues in the trigger's own persistent automation conversation. It retains earlier deliveries for that trigger but does not mix them into the main user conversation. Choose it when the task should build on previous automated runs.
isolated does not mean a fresh conversation for every invocation. Both
destinations retain context; they retain different histories. Avoiding visible
interruption is a secondary consideration. The primary choice is which history
the work requires.
The step should normally respect the selected trigger's configured destination. A workflow needing both histories should use two clearly named triggers instead of silently overriding one trigger's behavior.
Executing a Crew step consists of four core operations:
- Invoke the selected Crew trigger with the rendered instruction, declared workflow inputs, and a stable idempotency key.
- Poll or await the trigger run until it succeeds, fails, is stopped, or reaches the configured timeout.
- Read the exact final assistant response recorded for that trigger run.
- Save the text response in the calling workflow's current iteration folder.
For example, with variable groups enabled:
Workflow/example/runs/iteration-0/production/execution/review-with-rts/response.md
Active runs execute in iteration-0 (older numbered folders are archives);
without groups the step folder sits directly under runs/iteration-0/execution/.
The execution record should also retain operational metadata—Crew project ID, trigger ID, Crew run ID, session ID, status, timestamps, and error details—in the workflow's existing progress/run metadata. The Crew does not have to emit JSON for this metadata.
Token spend is attributed to both sides: the Crew run records its caller (workflow ID, run ID, step ID) so Crew-side cost history shows what each caller spent, and the workflow run's cost breakdown includes the crew step's tokens so the run shows its true cost. It is one spend visible in two places, each side labeled with the other — never silently absorbed by either side.
The delivery payload should include at least:
{
"source": "workflow_step",
"workflow_id": "release-pipeline",
"workflow_run_id": "9f3c2e1a-7b4d-4c8e-a1f2-3d5b7c9e1a4f",
"workflow_step_id": "review-with-rts",
"group": "production",
"instruction": "Rendered workflow-specific instruction",
"inputs": {
"pr": {
"number": 87,
"repository": "course_designer"
}
}
}group is the calling workflow's variable group, for correlation only.
workflow_run_id is the calling execution's immutable execution identity, not
its run folder: run folders are reused between executions, so only the
execution identity keeps one execution from adopting another's Crew delivery.
Payload data cannot override Crew permissions, tools, trigger ownership,
conversation destination, or folder guards.
An external caller invokes a public Crew trigger through its authenticated HTTP
endpoint. A workflow running inside the same deployment should use a
platform-internal trigger and reuse the same trigger dispatcher directly,
rather than sending an HTTP request to itself or placing trigger secrets in
plan.json.
Internal dispatch must produce the same trigger delivery, conversation, history, run status, final response, and idempotency behavior as the hardened public endpoint (see Crew trigger hardening).
A platform-internal trigger is a first-class trigger binding for communication between a Workflow and a Crew on the same deployment. It has no public URL and issues no bearer or GitHub secret. Its authority comes from an explicit caller binding checked by the platform on every invocation.
A binding is not a separate object: it is kind: "internal" plus a caller on
an ordinary trigger, stored in the same trigger list as public triggers. The
target is implicit — whichever manifest or schedule list holds the trigger —
so deleting or disabling the trigger automatically retires the binding and
orphaned permissions are impossible.
An illustrative Crew-side binding is:
{
"kind": "internal",
"id": "5f16e1fa-7ddd-4ee5-8311-63b34745ad46",
"name": "Release workflow reviewer",
"enabled": true,
"message": "Review the pull request supplied in the delivery payload.",
"caller": {
"type": "workflow",
"id": "release-pipeline"
},
"run_destination": "isolated"
}The reverse binding is a workflow schedule entry with kind: "internal" and
caller.type="crew". Bindings are directional and grant invocation only. They
do not grant write access to the target's files or management access to its
configuration.
Both sides manage bindings through their existing trigger tools, extended
with kind: "internal" (no secret issued, no public path, caller required):
- Workflow Builder creates an internal Crew trigger while adding a Crew step.
- Crew creates an internal workflow trigger when asked to invoke an attached workflow.
- Either side can list bindings; owners can disable, re-enable, or remove them. Existing trigger UIs show internal triggers with an internal badge and no URL.
Normally, creating a binding requires trigger-edit rights on the target side plus read access on the caller side. Crew-to-Workflow has one narrow exception: an authorized read-only workflow attachment may create the secretless internal binding scoped to that exact Crew when it first invokes the workflow. It cannot create public triggers or manage unrelated triggers. Invocation rechecks both resources, their attachment relationship, and the binding's enabled state. The public HTTP trigger endpoints skip internal triggers entirely: they are invokable only through internal dispatch. Copying either resource never silently copies cross-project authority.
Crew has one persistent project workspace. It does not have workflow iteration folders, and this feature must not add them.
Before a Crew step can run, the target Crew project must be attached to the workflow using the existing attached-folder mechanism. The attachment is read-only. The attachment is the stable namespace through which downstream steps read Crew files; it is not itself the permission. Folder grants store a fixed path and never recheck project access, so the workflow must verify Crew access before the run starts and again before each crew step executes, using the same permission check as trigger invocation. If access is gone — revoked, unshared, moved, or deleted — the run fails fast with an actionable error before any step reads through the stale grant.
If the Crew returns:
The detailed review is in reports/pr-87.md.
and the attachment alias is rts-reviewer, a downstream step can read:
rts-reviewer/reports/pr-87.md
The workflow can read the file but cannot modify it. The file remains owned by the Crew and may change later because Crew storage is persistent and mutable.
The first version does not infer an output_files list from natural-language
responses and does not automatically copy Crew files into the workflow run.
Small results may exist entirely in response.md. For large results, the
Builder can instruct the Crew to save files at a known Crew-relative location
and include those paths in its response.
If immutable file snapshots become necessary later, they should be an explicit workflow feature with declared paths. They must not be introduced by parsing arbitrary response prose.
The integration is bidirectional. A Crew can invoke an attached AgentWorks workflow through a selected workflow trigger. This is useful when a persistent, human-maintained specialist needs a deterministic pipeline for testing, rendering, deployment, extraction, or another bounded operation.
The runtime flow is:
- The Crew selects an attached workflow and an enabled internal trigger.
- It creates or uses a platform-internal trigger binding scoped to that Crew.
- It sends a JSON payload with a stable idempotency key.
- The workflow executes in its own
iteration-<n>-hookfolder. - The Crew polls or awaits the trigger run until it becomes terminal.
- The Crew reads the returned status, inline step outputs, progress, and artifact references.
- The Crew can summarize the result, update its persistent files or database, and update its dashboard.
The workflow trigger already defines its route selections, optional single-step target, variable groups, input mode, and execution contract. The Crew payload cannot override configuration that the trigger did not explicitly expose. The read-only attachment is sufficient to discover triggers and to create or reuse one secretless internal binding for the exact Crew, matching the inverse Workflow-to-Crew mechanism. Public trigger secrets remain protected, public triggers cannot be invoked through this path, and the attachment grants no general workflow editing or trigger-management access.
An illustrative invocation is:
{
"workflow_id": "course-designer-tests",
"trigger_id": "smoke-test-trigger",
"payload": {
"pull_request": 87,
"environment": "dev"
},
"wait": true,
"timeout_seconds": 1800
}The completed result can contain the workflow's normal structured outputs:
{
"status": "completed",
"run_id": "iteration-68-hook",
"terminal": true,
"steps": [
{
"step_id": "test",
"outputs": {
"result.json": {
"passed": 42,
"failed": 0
}
}
}
],
"artifacts": []
}Small JSON and text outputs can be read from the polling response. Large files remain workflow run artifacts and use the workflow trigger's existing signed download contract. When the workflow is attached read-only to the Crew, the Crew can also inspect the completed hook iteration through that attachment.
The conceptual distinction is:
- Workflow to Crew calls a persistent specialist and receives a natural text response.
- Crew to Workflow calls a deterministic pipeline and receives structured run status, step outputs, and artifacts.
The Builder should be able to:
- List Crews accessible to the current workflow owner, including icon, name, identity, and stable ID.
- Inspect a Crew's triggers, saved instruction summary, enabled state, and conversation destination.
- Attach the selected Crew to the workflow as read-only after an explicit configuration action.
- Create, add, update, and remove Crew steps.
- Explain whether the selected trigger uses main Crew history or its own persistent trigger history.
- Validate that the Crew attachment and trigger still exist before saving the plan.
The Builder tools are:
-
list_accessible_workflows(existing discovery: workflows and Crew projects with identity) -
manage_crew_trigger(create/list/update/delete Crew triggers, extended withkind: internal; internal bindings default to the calling workflow) -
manage_crew_attachment(attach/list/detach Crews read-only) -
add_step/update_step/delete_plan_stepswithtype: crew(plan editing, with crew field schemas)
The Crew runtime has the corresponding workflow tools:
list_attached_workflowslist_workflow_triggers-
manage_workflow_webhook(the existing workflow trigger tool, extended withkind: internal) run_workflow_triggerget_workflow_trigger_run
The Builder should help write instructions that state:
- which Crew memory and skills to consult;
- what runtime input to process;
- whether to update Crew files, database, or dashboard;
- when a short text response is sufficient;
- where to save large outputs and how to reference them in the response.
The Crew node should show the Crew icon and name, the selected trigger, its conversation destination, and the response output.
Example:
🚀 RTS PR Reviewer
Trigger: Review pull request
Conversation: Trigger history
Output: response.md
The configuration view should contain:
- Crew selector with icon, name, identity, and ID;
- trigger selector;
- clear retained-context explanation;
- workflow-specific instruction editor;
- context dependency selection;
- response filename;
- timeout;
- next step;
- read-only attachment status and action.
The UI should not imply that Crew files are copied into the workflow run.
Run-time observability lives in the execution views, not the plan. The crew step's run entry must link its recorded Crew run ID to the exact Crew turn — transcript and timing one click away. The execution logs gain a crew category with the polling timeline (queued, turn started, answer recorded) and the final response, like other step types have.
- Never store a plaintext trigger secret in the workflow plan.
- Platform-internal triggers never issue a public endpoint or plaintext secret.
- Every internal invocation must match the binding's exact caller and target.
- Check Crew and trigger access when the step is configured and again when it executes.
- Require the target Crew to be attached read-only before execution.
- Re-verify Crew access before the run starts and before each crew step executes; a revoked, moved, or deleted Crew fails the run fast, before any step reads through its attachment.
- Keep the workflow's write paths separate from the Crew attachment.
- Reject deleted or disabled triggers with an actionable step error.
- A copied workflow must not inherit authority to call a Crew its new owner cannot access.
- Resolve all referenced Crew paths inside the attachment root; traversal and absolute-path escape are forbidden.
Derive the trigger delivery identity from the workflow execution identity, group, step ID, and step attempt. Retrying the same attempt must recover the existing Crew delivery instead of creating duplicate work. A deliberate new step attempt receives a new delivery identity.
A persistent Crew conversation must process one turn at a time. When the selected conversation is occupied, the delivery is accepted and queued as the next turn; the step shows a queued state until the turn starts. It must not interleave two turns in the same conversation. Mid-turn steering is for corrections to the current job, not for new deliveries: every delivery gets its own turn and its own recorded final response.
Out of scope for the first version. Stopping the workflow ends the step's wait; the accepted Crew delivery runs to completion on its own. Crew-side cancellation may be added later.
The workflow step fails when the trigger is inaccessible, disabled, deleted, or completes with an execution error, or when the delivery is still queued or running beyond the timeout. The step error should include the Crew and trigger names plus the recorded run ID when one exists.
The public Crew trigger currently accepts a delivery and records the final response. A complete external call-and-poll contract should expose a status resource equivalent to:
POST /api/hooks/product/{trigger_id}
GET /api/hooks/product/{trigger_id}/runs/{run_id}{
"run_id": "...",
"status": "completed",
"terminal": true,
"final_response": "Review complete. Details are in reports/pr-87.md.",
"error": ""
}The internal workflow dispatcher may await the same service directly, but the observable run contract should remain consistent with the public API.
Crew triggers were built as fire-and-forget deliveries into a chat, while workflow triggers were built for programmatic callers (idempotent recovery, pollable status). A workflow stepper is a programmatic caller: it must distinguish queued from running from lost, and it must reattach after its own retries. The gaps below must be closed in the shared trigger layer, so both the public endpoint and internal dispatch offer the same accept → poll → terminal contract. Restart survival is explicitly out of scope: the system does not promise it anywhere else either.
Delivery dedupe is currently a process-local map that returns no run status, and run appends have no duplicate check, so a repeat can fork history under one run ID. Dedupe must consult the run store (check-then-append under the existing run-file lock), and a duplicate delivery must return the existing run ID with its current status, as workflow triggers do. No restart promises: the guarantee holds while the server is alive.
When the trigger conversation is occupied, the run is currently rejected
before any run record is written, while the public endpoint has already
returned 202 — a poller waits on a run ID that will never exist. There must
be no busy rejection at all: the delivery is accepted, recorded with a
queued status, and executed as the next turn in that conversation. The
runtime's mid-turn steering is for corrections to the running job; queued
deliveries each get their own turn and their own recorded final response.
The run lock is keyed per trigger, but every crew_chat trigger of a project
shares one main conversation with the other triggers and the user.
One-turn-at-a-time holds for isolated triggers only. Main-chat dispatch
needs a conversation-level lock so two turns can never interleave in the same
conversation.
Crew runs record running, success, error, and stopped (plus partial
and interrupted); accept-and-queue adds a real queued state for accepted
deliveries still waiting their turn. The step's displayed states map onto
this vocabulary, with timed-out owned by the waiting stepper rather than
the Crew run.
Steps 1–10 are implemented. Stopping runs is out of scope: there is no stop tool, and an accepted Crew delivery runs to completion on its own.
- Harden the shared Crew trigger layer: idempotent redelivery that returns
the existing run with its current status, accept-and-queue with a real
queuedrun state, and conversation-level mutual exclusion for main-chat triggers. - Extend both sides' trigger models with
kind: internalplus a caller, reusing existing trigger storage, ownership checks, and management tools; public HTTP trigger endpoints skip internal triggers. - Add the Crew trigger status response and durable final-response lookup.
- Extract internal Crew-trigger and workflow-trigger dispatch interfaces from their HTTP handlers so callers can use them without secrets or loopback HTTP. Internal dispatch must surface disabled and deleted outcomes synchronously, with the same delivery, conversation, history, run status, final response, and idempotency behavior as the hardened public endpoint.
- Add
CrewPlanStepparsing, validation, marshaling, plan editing tools, and workflow execution dispatch. - Save the final text to the step's current iteration execution directory and connect it to normal context-output handling.
- Add Builder discovery and mutation tools for Crews, triggers, and read-only attachment setup.
- Add Crew tools to discover, create, invoke, and poll internal workflow trigger runs.
- Add the Crew canvas node and configuration UI.
- Update Builder and Crew reference material and product guidance so each side understands internal triggers, and so the Builder can choose between a Crew and an ordinary step and between the two retained conversation histories.
- A Builder can select an accessible Crew and one of its enabled triggers and add it as a workflow step.
- Workflow Builder can create and manage a Workflow-to-Crew internal trigger; no public URL or secret is created.
- Crew can create and manage a Crew-to-Workflow internal trigger for an attached workflow, invoke it, and poll it.
- The Builder establishes or verifies a read-only Crew attachment.
- A workflow run invokes the trigger exactly once for one step attempt and can recover safely after a retry.
- A duplicate Crew delivery returns the existing run ID and its current status; no second run is launched.
- An occupied Crew conversation accepts the delivery and queues it as the next turn; the step shows queued until the turn starts.
- Two turns never interleave in one Crew conversation, including the shared main chat.
- The step displays queued, running, completed, failed, and timed-out states, mapped onto the recorded Crew run statuses (a stopped run fails the step; its status stays visible in the execution logs).
- The Crew receives the rendered instruction and declared workflow input.
- Main-chat triggers retain the main Crew conversation; isolated triggers retain their own prior trigger-run conversation.
- The exact final Crew text is saved as
response.md(or the configuredcontext_output) inside the calling workflow iteration. - Downstream steps can read Crew-relative files through the read-only attachment and cannot write to them.
- No trigger secret appears in
plan.json, logs, frontend state, or step outputs. - Internal trigger invocation is rejected when caller identity, target identity, attachment, permissions, or enabled state no longer matches.
- A workflow run whose Crew access was lost after configuration fails fast before any step executes; no step reads through the stale attachment.
- Crew-step token spend appears in both the workflow run's cost breakdown and the Crew project's cost history, each side labeled with the other.
- A crew step's run entry links to its exact Crew turn, and the execution logs include a crew category with the polling timeline and final response.
- Crew projects remain persistent projects and do not gain iteration folders.
The implementation was reviewed against this design after steps 1–10 landed. It is not ready to deploy until the blocking findings below are resolved.
-
P0 — Current
maindoes not compile.server.goandlive_input_durable.goreferenceDurableAck,SupportsDurableAck, and provider durable-await functions that are absent from the pinned and locally replacedmcpagentdependencies. This blocks the server test package and deployment. Either land and pin the dependency API first or remove the incomplete integration from this release. -
P1 — Crew-step idempotency collides across workflow executions. The delivery key is currently built from workflow ID, selected run folder, and step ID. Normal executions reuse
iteration-0, so a later execution can adopt the successful Crew delivery from an earlier execution and return its stale response without running the Crew. Include the workflow's immutable execution ID in the delivery identity, while keeping that identity stable across retries of the same step attempt. -
P1 — Terminal success is visible before the response and usage. Crew-run persistence writes terminal
successfirst, then writes the final response and token usage in separate operations. A poll between those writes can return a successful run with an empty response and no cost data. Persist the terminal status, final response, usage, completion time, and error as one atomic run-record update, or make terminal status the final write after every required result field is durable. -
P1 — A stale Crew attachment can remain readable after access is revoked. Session folder grants are created directly from the attachment's stored workspace path. The run preflight validates only plans containing a Crew step, and access is not rechecked when an ordinary downstream step reads through the alias. Removing the Crew step while retaining the attachment, or revoking access during a run, can therefore leave the Crew workspace readable. Resolve and authorize attachment reads dynamically, or validate every attachment before granting its root and revoke the grant as soon as access changes.
-
P2 — The shared Crew-trigger mutation path does not authorize the caller workflow. The Builder tool performs this authorization before calling the shared save function, but the product-webhook REST endpoint reaches the same function directly. The shared mutation boundary checks only that the caller workflow ID exists. It must also require the requesting user to have read access to that workflow.
- Run the same Crew step in two separate executions that both use
iteration-0; assert that two Crew deliveries run and return their own responses. - Retry one step attempt with the same immutable execution identity; assert that it adopts the original delivery instead of starting another.
- Poll continuously while a Crew run finishes; assert that no terminal response is observable without its final response and token usage.
- Revoke Crew access before a run, between a Crew step and a downstream read, and after removing the Crew step while retaining its attachment; every read must fail closed.
- Attempt to create an internal trigger bound to an inaccessible workflow through both Builder tools and the REST endpoint; both paths must reject it.
- Crew workflow, orchestrator, attachment, and workflow-type Go tests passed.
- Frontend Crew tests passed: 11 tests across the Crew node, canvas presentation, and execution-log helpers.
- TypeScript compilation passed.
- The server test package could not build because of the P0 dependency/API mismatch above.
The original findings received the changes and regression tests listed below. The follow-up review found that attachment validation and delivery isolation still have uncovered cases, plus an additional failure-reporting bug; this resolution section does not constitute current sign-off.
-
P0 — resolved. The
maincompile breakage was fixed separately and the server package builds again. -
P1 idempotency — resolved. The delivery identity keys on the
workflow's immutable execution ID (
ExecutionID, falling back to the run folder only for bridgeless callers), stable across retries of one step attempt and unique across executions. Tests:TestRunCrewStepIsolatesExecutionsSharingRunFolder(two executions initeration-0get separate deliveries) andTestRunCrewStepRetryAdoptsSameExecutionDelivery(a retry adopts the original delivery). -
P1 atomic write — resolved. Run completion persists through one
UpdateScheduleRunResultread-modify-write holding the run-file lock, so terminal status, final response, usage, and completion time land together. Tests:TestUpdateScheduleRunResultSingleWrite(one store write) andTestUpdateScheduleRunResultAtomicity(concurrent pollers never observe a torn terminal record). -
P1 stale attachment — resolved. Attachment reads are validated
dynamically at every boundary: the run preflight checks every attachment
(binding shape plus live crew access) even when the plan holds no crew
step; session grants carry only validated roots; detach/attach reconcile
live sessions immediately; session refresh re-derives crew grants from
the live manifest; and the orchestrator re-validates the matched
attachment on every read. Tests:
TestPreflightCrewAttachmentsWithoutCrewSteps,TestDetachCrewAttachmentRevokesLiveSessionGrant,TestCrewAttachmentReadRootsSkipInvalidBindings,TestLiveCrewAttachmentGrantsSkipInvalidAttachments,TestRefreshWorkflowFolderAccessSessionReconcilesCrewGrants,TestCrewAttachmentReadFailsClosedAfterDetach,TestCrewAttachmentReadFailsClosedOnRetargetOrDelete, andTestValidateCrewAttachmentBinding. -
P2 REST auth — resolved. The shared trigger-save boundary requires
the requesting user to have read access to the named caller workflow, so
the Builder tool and the REST endpoint enforce the same rule. Tests:
TestManageCrewTriggerRejectsInaccessibleCallerWorkflowandTestSaveProductWebhookRejectsInaccessibleCallerWorkflow.
Known residual: a crew that is unshared mid-run while its workspace directory still exists on disk remains filesystem-readable to ordinary downstream steps until the run ends; crew steps themselves re-check access before every invocation, and the next run's preflight fails fast.
Reviewed against mcp-agent-builder-go f6ee03b72, including the earlier
Crew review fixes in 45f9e72d0.
The implementation is connected end-to-end: CrewPlanStep is parsed and
validated, the controller renders instructions and inputs, a server-owned
runner invokes the shared internal trigger dispatcher and polls its run, and
the controller saves the final response, operational metadata, and token
usage. Reusing the trigger dispatcher and atomic completion record is sound.
However, the three reproduced findings below prevent sign-off. They remain
open; this documentation update does not fix production code. Line references
refer to the reviewed revision.
Sources:
- crew_step_runner.go, lines 245–252: attachment preflight.
- crew_attachments.go, lines 77–91: stored binding validation.
-
crew_builder_tools.go:
crewAttachmentReadRootsandliveCrewAttachmentBindings.
Binding validation checks that the stored path contains a projects segment
and ends with the declared project ID. Preflight separately resolves and
authorizes that project, but discards its resolved workspace binding instead
of comparing it with the stored root. A path belonging to another owner can
therefore pass validation when its final project-name segment matches.
Reproduction: start with an accessible rts Crew belonging to owner, then
change only the saved attachment root from
_users/owner/Chats/Work/projects/rts to
_users/other/Chats/Work/projects/rts. Preflight succeeds and
crewAttachmentReadRoots returns the other owner's path as a read grant.
TestReviewCrewPreflightRejectsWrongOwnerRoot reproduced both failures.
This violates the intended separation between a stored attachment path and
actual authorization to read that workspace.
Required fix: derive the granted root from the freshly authorized project binding, or require exact canonical equality with that binding before granting or resolving the stored path. Apply this at grant creation and refresh/read boundaries. Retain a regression covering two owners with the same project-name suffix; checking only a different suffix or a non-project path is insufficient.
Sources:
-
crew_step_runner.go, line 74
and
crewStepDeliveryBase. - controller_crew.go: request construction passes both execution identity and group.
-
context_aware_bridge.go:
ExecutionIDremains fixed whileSetBatchContextchanges groups.
The delivery key contains workflow ID, immutable execution ID, and step ID,
but omits Group. Multiple variable groups within one execution consequently
share a delivery identity for the same Crew step. Once the first group
completes, a later group adopts its response instead of invoking the Crew with
its own rendered instruction and inputs.
Reproduction: seed a successful production-group delivery, then invoke the same
step and execution with Group=staging, a different run folder, and a different
instruction. The runner returns the production delivery ID and response.
TestReviewCrewGroupsDoNotShareDelivery reproduced the failure. The earlier
cross-execution regression varies execution IDs and does not cover this case.
Required fix: include the variable group in the stable delivery identity while preserving idempotency for retries of the same group and step attempt. Add a test in which two groups share one execution ID and must receive independent Crew deliveries and responses.
Source: product_schedules.go,
executeAutomationRun, lines 988–993; acceptance occurs in
product_webhooks.go,
deliverProductTrigger.
The dispatcher persists a queued run before starting the detached worker.
If the worker cannot resolve its conversation binding or open the conversation
registry, it returns before writing a terminal result. Its defer releases the
conversation reservation, but the accepted run remains queued with no worker.
The workflow therefore waits until timeout instead of receiving the actual
failure, and redelivery adopts the same stranded record.
Reproduction: pre-claim a queued run, force a conversation-policy setup error,
run executeAutomationRun, and read the stored status after it returns the
error. The record remains queued. TestReviewCrewSetupFailureBecomesTerminal
reproduced this early-exit behavior. Other failures at the same boundary, such
as failed conversation-store access, take the same return path.
Required fix: persist a terminal error against the original claimed run on every setup failure, even before a conversation/session ID is available. Add coverage proving that setup errors become pollable terminal failures and that retry recovery does not adopt a permanently queued record.
- Focused server tests covering Crew runners, preflight, Builder tools, attachments, internal dispatch, and atomic run results passed.
- Crew-focused tests passed in
pkg/workflowtypes,pkg/orchestrator, andpkg/orchestrator/agents/workflow/step_based_workflow. - Eight frontend tests passed across
CrewNode.test.tsxand execution-loghelpers.test.tsx. - Three temporary regression tests failed on the intended invariants, confirming the findings above. They were removed after reproduction and are not committed regression coverage.
- No production code was changed. Live Crew execution, the full repository suites, and TypeScript compilation were not exercised in this follow-up.
Current disposition: changes required. Fix these findings and retain their regression coverage before sign-off. The previously documented mid-run access revocation limitation also remains open; the existing passing tests do not establish that every attachment access is dynamically authorized.
All three findings were confirmed against the code and are fixed below, each with permanent regression coverage.
-
P1 wrong-owner grant — fixed. The run preflight now compares the stored
attachment root against the freshly authorized project binding and fails
fast on mismatch; session grants derive each root from that binding and
grant nothing on mismatch, revoked access, or missing user. Tests:
TestPreflightCrewAttachmentsWithoutCrewSteps(wrong-owner case),TestCrewAttachmentReadRootsSkipInvalidBindings(wrong-owner omission), andTestCanonicalCrewAttachmentRoot. -
P1 shared group identity — fixed. The delivery identity is now
workflow + execution + group + step, matching the design's idempotency
contract; retries of the same group and step attempt stay idempotent.
Test:
TestRunCrewStepGroupsDoNotShareDelivery. -
P2 stranded queued run — fixed.
executeAutomationRunrecords a terminal error against the pre-claimed run on every conversation setup failure (recreating the record if retention trimmed the pre-claim), so pollers receive the failure and redelivery adopts a terminal record instead of a stranded queue entry. Cron runs claim nothing up front and keep their existing behavior. Test:TestExecuteAutomationRunSetupFailureBecomesTerminal.
Residual: cross-owner equality is enforced server-side, where the user-scoped binding can be resolved. The orchestrator's local read path still validates shape and existence only; a mid-run hand-edit of the manifest to another owner's path is not re-authorized there.
Implemented 2026-09-20. Workflow Builder creates a Crew while the user is
building a workflow through the create_crew tool (CreateCrewProject
service). This removes the multi-screen setup in which the user must create a
Crew separately, return to the workflow, attach it, create a trigger, and
select several IDs.
The simple user flow is:
- Builder recognizes that a step would benefit from a dedicated specialist and asks, for example, "Create a Release Reviewer Crew with GitHub access?"
- The user approves the Crew and the requested connections.
- Builder creates a useful starter Crew, connects it to the workflow, and adds the configured Crew step.
- The user can immediately test the Crew manually or run the workflow, then improve the Crew's instructions, skills, memory, and integrations over time.
This makes a Crew a more capable, user-testable specialist than an ordinary message sequence or execution-only subagent. It has a dedicated workspace, memory, skills, integrations, and conversation that the user can open and refine independently of the workflow.
One Builder action should create the usable integration, including:
- the Crew name, purpose, starter instructions, and selected skills;
- the MCP connections the Crew uses by default;
- references to the authorized secrets required by those connections;
- one enabled platform-internal trigger restricted to the creating workflow;
- a read-only Crew attachment on the workflow;
- the creating workflow in the Crew's
workflow_context_paths, so the new Crew automatically gets read-only context on the workflow it serves; and - a Crew step already configured with the new Crew, trigger, and attachment
(returned as an exact
add_stepconfiguration; the Builder issues the call through the gated plan path).
Secrets are passed only as references to existing authorized secret or connection records. Raw secret values must not be written into the workflow plan, Crew files, tool response, or Builder conversation. Builder should show the requested integrations in one approval step and ask the user to connect only anything that is missing.
The default trigger uses an isolated Crew conversation so automated workflow
runs do not clutter the main Crew chat. It is internal, accepts the step
instruction and workflow inputs, and is automatically selected by the new Crew
step. The creation result returns the Crew ID, trigger ID, attachment alias, and
step configuration so Builder can finish without asking the user to copy IDs.
The two links are symmetric and both read-only: the workflow reads the Crew
through its attachment, and the Crew reads the workflow through
workflow_context_paths (always mounted read-only and authorized through
the same boundary as transient references). Neither side can write to the
other. The user can remove the workflow from the Crew's context paths later
through the existing picker; the trigger binding and attachment are unaffected.
Creation should initially happen only while building the workflow and after the user approves it. Automatically creating permanent Crews during workflow execution is deferred; retries, loops, and parallel groups could otherwise fill the user's Crew list with resources they did not explicitly choose to keep.
Implemented. create_crew mints the Crew (UI-identical manifests, starter
MEMORY.md brief, skills/servers/secret selections, default LLM config),
creates the enabled internal trigger bound to the workflow, attaches the Crew
read-only, seeds the creating workflow into the Crew's context paths, and
returns the exact step configuration; the Builder then issues add_step
through the normal gated path. Retries under the same idempotency key adopt
every record instead of duplicating. Only the work profile is supported.
One deliberate deviation from the plan below: the service does not write the
plan step itself. Direct executor invocation would bypass the schedule
collision guard and phase policies that wrap add_step; returning the step
configuration keeps plan mutations on the gated tool path. The user still
approves once and copies no IDs.
If a later use case requires fresh runtime instances or parallel Crew fan-out, add that as a separate lifecycle feature now that Builder-created Crews exist.
Goal. Let Workflow Builder create a new Crew while the user builds a workflow: one user approval produces a usable, testable specialist — Crew project, skills/servers/secret references, an enabled internal trigger restricted to the workflow, a read-only attachment, and a configured crew step.
Success criteria. From Builder chat, a user goes from proposal to an approved, working crew step without copying IDs or leaving the workflow. The created Crew is immediately openable and testable in normal Crew chat, and a workflow run using it passes preflight and executes. Secrets travel as references only. Retrying the creation never mints duplicates. Missing OAuth connections surface as a pending list.
Current facts. Creation today is client-side only
(createProductProject in frontend/src/platform/chat/productProjects.ts
plus createWorkSession): it writes workflow.json, product.json, and a
code/ folder through generic planner-file APIs, with no server validation,
idempotency, or wiring. Reusable server pieces: manage_crew_trigger
(internal + caller binding), manage_crew_attachment, add_step(type="crew"),
list_accessible_workflows, list_secrets / manage_global_secret,
list_mcp_servers, list_skills / search_skills, and the
updateProductSelected* writers. Builder approval is conversational
(blocking human_feedback is forbidden in Builder mode).
Constraints and non-goals. Runtime creation stays out of scope. No new frontend in v1 (chat-only flow). No deletion cascade: a created Crew survives workflow deletion. Non-goals: ephemeral crews, run-scoped conversations, migration of existing Crews, frontend creation dialog.
Key decisions.
- One server action, not composed tool calls: a single
create_crewBuilder tool backed by one service method (create, selections, trigger, attachment, step). Only server coordination gives atomicity and idempotency. - Conversational approval plus idempotency key: Builder proposes in chat, calls the tool after the user's approving reply with a client-generated key.
- Idempotency via durable receipt: the key claims a receipt freezing crew ID, path, alias, and step ID (supersedes the earlier deterministic-path sketch, which could not survive a changed title); re-entry returns the existing record, a changed payload is a conflict. Duplicate display names stay allowed.
- Secrets as validated references only: the schema accepts names, the service verifies existence and authorization, and no value-bearing fields exist.
- Selections land through the existing
updateProductSelected*writers. - Trigger defaults: internal,
isolated, enabled, caller-bound to the creating workflow; alias defaults to the slugified Crew name.
Work plan (strictly ordered).
- Creation core (backend): user-scoped projects root, slug+id path,
product.json,workflow.jsonruntime manifest,code/folder, session id; input validation; idempotent re-entry. Mirror the UI file layout byte-for-byte, with the creating workflow pre-seeded in the Crew'sworkflow_context_paths. - Starter content plus selections: seed starter instructions/purpose; apply skill/server/secret selections; reject unknown secret names; resolve default LLM config server-side (fallback: omit, profile default applies).
- One-shot wiring: internal trigger and read-only attachment via the
internal implementations behind the existing tools; return all IDs plus the
exact step configuration (the Builder writes the step via
add_step, so plan policies apply — see Current state). -
create_crewBuilder tool plus guidance: schema per the creation contract plusidempotency_key; Builder-mode gating; proposal format and pending-connection reporting; the sequence < orchestrator < crew proposal ladder. - Docs: flip this section to implemented; record the orphan rule and pending-connections behavior.
Validation. Creation/wiring/preflight Go suites green; gofmt and server
build clean; manual Builder-chat E2E (propose, approve, verify Crew, test in
chat, run workflow, retry approval with zero duplicates, missing-OAuth
pending list).
Risks. Partial failure across six writes (mitigate with ordered writes plus idempotent re-entry; rollback is deregistering the tool — created crews are ordinary crews). Over-eager proposals (guidance ladder). Secret-name confusion (fail closed).
Reviewed AgentWorks commit 85514dc15. Status: changes required before the
Builder-created Crew flow is ready for users. The product direction is good:
one approval should create the specialist, internal trigger, read-only link,
and ready-to-add workflow step without making the user copy IDs. The happy-path
production package builds, but the current implementation does not yet satisfy
that simple flow reliably.
crew_creation_test.go calls mock.hasFolder, but mockWorkspaceAPI has no
such method, so go test ./cmd/server fails to compile. Supplying that helper
exposes a second test-fixture gap: workspace_mock_test.go does not implement
POST /api/folders, so every successful creation case fails while creating the
Crew's code/ folder. Fix both mock behaviors before treating the creation,
wiring, or idempotency tests as evidence.
CreateCrewProject validates installed skills, but MCP server, project secret,
and global secret inputs receive shape validation only. Unknown, disconnected,
or unauthorized names are therefore written into the Crew manifest. This
conflicts with the creation contract above, which requires references to
existing authorized records and a pending list for missing connections.
This is also a scope problem for ordinary secrets: a workflow-scoped secret in the creating workflow does not automatically exist in the new Crew workspace. The creation result may look complete while the Crew receives an empty value at runtime. Validate each MCP and secret reference in its real scope before writing selections. Return missing or disconnected integrations explicitly so the Builder can ask the user to connect only those items. Do not pass secret values through Builder chat.
The Crew ID is derived only from idempotency_key, while the workspace path is
derived from both the title and that key. Reusing a key with a changed title can
therefore create a second directory carrying the same Crew ID. Attachment
wiring then fails and leaves the second project stranded. Resolve an
idempotency key to one stored creation receipt or one path independent of
mutable proposal fields; reject a changed payload or return the original Crew.
The occupied-path fallback has the same principle problem because it chooses a new random ID and path on every retry. Persist that decision so re-entry adopts the same Crew.
Duplicate display names are allowed, but the default attachment alias and step
ID are direct title slugs. Creating a second Crew with the same title writes its
project and internal trigger, then fails because the first Crew already owns
the alias; its default step ID would collide as well. Select available,
deterministic suffixes such as release-reviewer-2 and
crew-release-reviewer-2 before creating any resources, and return those exact
values to the Builder.
-
go build ./cmd/serverpassed. -
gofmt -dandgit diff --checkpassed for the reviewed change. - Crew-related
pkg/workflowtypes,pkg/orchestrator, andpkg/orchestrator/agents/workflow/step_based_workflowtests passed. - The new
cmd/serverCrew tests failed as described above, first at compile time and then at the missing folder-create mock after temporarily supplying the absent helper.
Recommended acceptance is intentionally small and user-focused: the server tests run; unavailable MCPs and secrets are reported clearly; the same retry key cannot mint another Crew; and two Crews with the same display name both finish with usable aliases and step IDs.
All four findings above are fixed; the acceptance checklist now holds:
- The folder-create mock and
hasFolderhelper ship with the change, so the committedcmd/serverCrew tests compile and run. - MCP servers resolve against the platform catalog (unknown names fail; configured-but-disconnected servers are selected and returned as pending), project secrets must resolve to stored user secrets or globals (workflow- scoped names fail with a carry-over hint), and unknown globals fail. All availability checks run before any crew resource exists.
- The idempotency key claims a durable receipt freezing crew ID, path, alias, and step ID. A changed proposal under a claimed key is a conflict (new key required); the occupied-path fallback persists in the receipt, so re-entry adopts the same crew.
- Attachment alias and step ID are resolved to free deterministic values
(
slug-2,crew-slug-2) before anything is created, with the default step paired to the selected alias; explicit collisions fail before writing.
First production run failed deterministically: create_crew wrote the crew
manifests, then code/ folder creation was denied with ACCESS DENIED
because it went through the guarded workspace client, which enforces the
Builder session's sandbox (Workflow/<folder> only). The fix uses the raw
createWorkspaceFolder helper instead — the session guard confines the
model's direct file access, not the server's own authorized tool writes, and
every other creation write already used raw helpers. A regression test runs
creation under a Builder-style guard. Retrying with the same idempotency key
adopts the receipt and converges the half-written crew instead of duplicating
it.
Two production follow-ups. New crews inherit the creating workflow's model (provider profiles resolve through the same defaults the engine uses; explicit builder choices carry over verbatim), falling back to the profile default only when the workflow names nothing usable. And the server-owned crew runner now binds on every authenticated run path — UI runs, Builder chat runs, live input, schedules, webhooks — instead of only runs carrying execution options, so crew steps test identically from Builder; the old error text wrongly claimed scheduler-only execution.
Workshop follow-up the same day: Builder's own execute_step and run_full_workflow build their own execution options and never saw that binding, so live logs showed the same "does not bind a Crew runner" failure. The server now injects the runner into WorkshopConfig; session init carries it onto the controller (execute_step reads the controller's options untouched) and run_full_workflow copies it onto its fresh controller. Binding also falls back to the single-user default identity like every other per-user lookup, instead of requiring JWT claims.
Caller-stamp follow-up: triggers are bound to the workflow manifest ID at creation, but dispatch presented the controller's random per-construction workflow ID, so every invoke failed with caller mismatch. Crew steps now stamp the manifest ID read from workflow.json (same ReadWorkspaceFile pattern as the code-layout loader); the random ID stays for human-feedback correlation and as the no-manifest fallback. Side benefit: the delivery idempotency key is now stable across sessions, so retried runs adopt the live crew run instead of invoking twice.
Completion-capture follow-up: the crew ran 4m14s and wrote its alert file, but the run recorded success with a null final response, so the workflow step crashed saving the empty file. Turn completion already funnels through one reader (finalResponseForExecution, serving schedules, triggers, and bots). Live event-store dump showed the real defect: the reader matched only the query execution ID, but main-agent turns scope their events to main: — the unified_completion with the full summary sat in the store unmatched. The reader now matches both scopes, newest first, and falls back to the newest llm_generation_end content — the same fallback scheduledTurnProducedResponse uses.
Delivery-key follow-up: the idempotency key covered workflow/run/ group/step only, so retesting a step after switching its trigger to crew_chat silently adopted the old isolated run (same-second "success", nothing in chat). The trigger ID and normalized destination now join the key: identical retries still adopt, retargets always fire.
Auto-synced from docs/ on main. Edit there, not here.