Repository navigation
Description
Written-by: FlyBot Product
plugins/workflow/** belongs to the workflow module, not to us: this is a defect report. We do not implement it and will not open a PR for it.
What happens
Copying one factory preset three times produces three workflows with the identical name (发布交付流水线 副本). Names are the address used by run_workflow, by a board workflow:<name> token and by assignee: <name>; every one of those paths resolves with a first-match lookup, so a run started by name silently lands on the first copy and the other two can never be started by name at all. A user cannot tell the three rows apart in Settings either (same name, same preview), and cannot tell which one a run will use.
The same collision is reachable without the preset path: create never enforces a unique name, and rename does not either, so two hand-written workflows may share a name — same silent first-match routing.
Reproduction (in-process service, no host)
First run on b20524fe; re-checked on live main 5daf756b (2026-10-27T11:57:54Z), where plugins/workflow/src/service.js and match.js are unchanged and the probe gives the same shape (ids differ per run; the 5daf756 run: wf-96c7edaf / wf-b953ae5e / wf-e5cd96bd, only the first reachable by name).
const svc = createWorkflowService({ persist: false, now: () => 1_700_000_000_000, bots: () => roster })
const tpl = templateCatalog()[0]
for (let i = 0; i < 3; i += 1) svc.create({ template: tpl.id })
const copies = svc.state().workflows.filter(w => w.name === `${tpl.name} 副本`)
const ran = svc.runNamed({ workflow: `${tpl.name} 副本`, brief: 'Ship it', task: '#1' })
const started = ran.workflows.filter(w => (w.activeCount ?? 0) > 0).map(w => w.id)Output:
copies = [ 'wf-906f069e', 'wf-e5d84bcb', 'wf-d904162f' ]
copies named "发布交付流水线 副本" = 3
runNamed("发布交付流水线 副本") started = [ 'wf-906f069e' ] | first copy = wf-906f069e
copies 2 and 3 reachable by name = false false
A second probe with two hand-written workflows both named 发布 (space-free name, so the token path is not truncated):
ids = [ 'wf-052433b9', 'wf-66e5b2a4' ]
runNamed("发布") started = [ 'wf-052433b9' ] | second copy started = false
board token "workflow:发布" routes to = wf-052433b9 | second = wf-66e5b2a4
assignee "发布" routes to = wf-052433b9
Root cause
plugins/workflow/src/service.js:672— the only dedupe for a template copy isstate.workflows.some(item => item.name === base) ? cleanLine(${base} 副本, 80) : base. It compares the base name once and never compares the candidatebase 副本against the store, so copy FlyBot plugin layer (ctx.flybot) with social and tree plugins #2 and copy FlyBot: plugin architecture (umbrella) #3 both compute the samebase 副本.create(:665-713) has no unique-name check of any kind (a non-template create takescleanLine(body.name, 80)as-is).rename(:624-639) also has none: it checks readability and non-empty only, then assignsitem.name = name.findWorkflow(:142-146) isstate.workflows.find(item => item.id.toLowerCase() === wanted || item.name.toLowerCase() === wanted)— first match wins, no ambiguity signal.plugins/workflow/src/match.js:28-31is a second copy of the same first-match lookup, and:53-60uses it for theworkflow:<name>token,task.workflowId/task.workflow, andassignee.plugins/workflow/index.js:111-118— therun_workflowtool takesworkflow: 'Workflow id or exact name'and callsservice.runNamed, so a model or a user passing the name gets the first match.- Client side:
core/src/client/workflows.js:371-386useTemplatepostsworkflow/createwith only{ template }, so two clicks produce the first pair of identical names;core/src/client/workflows.js:392-394confirmRenameonly rejects an empty name and then postsworkflow/rename, so a rename onto an existing name is accepted.
Expected
- Names are unique inside the store. A template copy picks the next free name (
X,X 2,X 3, or副本 2,副本 3), not the sameX 副本every time. createandrenamerefuse a name that is already taken and answer with an explicit error (A workflow named "X" already exists), instead of storing a duplicate.- If a legacy store already carries two workflows with the same name,
runNamedmust not silently pick the first: it returns{ ok: false, error }naming both candidate ids (or returns the candidates), so the caller can retry by id. - The Settings rename dialog surfaces that error text. The rename/create error pass-through in
core/src/client/workflows.jsis ours to make; the request issue is not split for it — whoever implements the workflow-module side should state in the PR which half the host did.
Acceptance (deterministic red today)
One test that covers all three cases:
- three
create({ template })calls in a row produce three distinct names (today: three identical发布交付流水线 副本); renameonto an existing name answersok === falseand leaves the store unchanged (today:ok === true);- with a store that already holds two same-named workflows,
runNamed(name)answersok === false(or lists the candidate ids) instead of starting the first (today: startswf-…Import the DS Bot plugin codebase from DeepSeek-Bot #1 silently); - after the fix, each of the three copies can still be started by id, and a start by name resolves to exactly one workflow or is refused.
Reachable
Not visible to users yet: the instance checkout is e37f1f8, whose build predates the Workflows surface; the defect arrives with the next milestone update. It is live on main (5daf756b, 2026-10-27T11:57:54Z) and reproducible in-process as above — no host, no browser, no credentials needed.
Dedupe (all states, open + closed)
workflow duplicate name→ 46 results, no match (top hits:#1414ephemeral map lost,#1416denied step counted as handed,#1272engine meta).workflow rename same name→ 23, no match (top hits:#1445,#1440,#1434closed rename+delete,#1409).workflow name collision→ 2, neither about workflows (#820header seats,#1328board unify).two workflows same name→ 92, no match (#1378/#1432read-only flowchart,#1425reap test wall-clock).copy workflow template→ 10, no match (#1378,#1374closed page rewrite,#1291closed engine step 1).副本→ 12, no match (#1396preset loses its "Use this" button once it has a live copy,#1287copy a scheduled task, both different).
Closest existing issues and why they are not this one: #1434/#1435 (rename and delete exist; no uniqueness), #1396 (a preset with a live copy loses its button — about the template row, not about two user workflows sharing a name), #1414 (a run's ephemeral map is lost — a lane/reap concern), #1441 (delete reads the emptied state.runs — a different function and a different symptom).
Activity
HugoXK commentedon Oct 27, 2026
Closing: this was filed under the superseded "report only, we do not implement
plugins/workflow/**" policy (that line is still in the body). No implementation was started and there is no PR, so under the new rule the issue comes down rather than sitting as a claim without a PR. Will re-file with the reproduction and the PR together when a Dev picks it up.