windmill-labs/windmill · error · PlanSlotTakenError
PlanSlotTakenError
Error message
PlanSlotTakenError
What it means
PlanSlotTakenError is thrown by artifactsState.create when a plan-role create targets a session that already has a plan artifact. A session holds exactly one plan: the id is deterministic (planArtifactId(sessionId)), so a second create collides with the existing document inside mutateArtifact, and the error carries that existing plan because the useful next step is to revise it.
Source
Thrown at frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts:177
kind: input.kind ?? 'md',
role: input.role,
approvedVersion: input.approvedVersion,
name: input.name,
content: input.content,
createdAt: now,
updatedAt: now,
version: 1
})
// A plan's id is the session's, so a second one cannot be minted; the slot check happens
// on the row this write is about to replace, inside the transaction that replaces it.
const id = input.role === 'plan' ? planArtifactId(sessionId) : randomUUID()
const { outcome, artifact } = await mutateArtifact(id, (existing) => {
// Before the slot check: a posture that may not mint a plan at all is the more useful
// thing to say, and it holds whether or not the session already has one.
if (input.role === 'plan' && opts?.canWritePlan?.() === false) {
throw new PlanWriteRefusedError()
}
if (existing) throw new PlanSlotTakenError(existing)
const created = draft(id)
return { artifact: created, snapshots: [snapshotOf(created, 1)] }
})
// An ordinary artifact degrades unpersisted; a plan cannot. Returning one the database
// refused would let the user approve a plan that disappears on reload.
if (input.role === 'plan' && outcome !== 'saved') throw new ArtifactPersistenceError()
const written = artifact ?? draft(id)
this.#reflect(written)
return written
}
/**
* Merge changes into an existing artifact. Returns undefined if `id` is unknown, or if
* `opts.sessionId` is given and the artifact belongs to a different session.
*/
async update(
id: string,
input: UpdateArtifactInput,View on GitHub (pinned to e474e8803c)
Solutions
- Catch PlanSlotTakenError and use error.plan (or planArtifactId(sessionId)) to update/revise the existing plan instead of creating one
- Read the existing plan first (getVersion/getArtifactVersion) and only create when none exists
- Clear/replace the session's plan through the update path so approval state carries over correctly
Example fix
// before
await artifacts.create({ role: 'plan', sessionId, content }) // throws when slot taken
// after
try { await artifacts.create({ role: 'plan', sessionId, content }) }
catch (e) {
if (e instanceof PlanSlotTakenError) await artifacts.update(e.plan.id, { content })
else throw e
} Defensive patterns
Strategy: try-catch
Validate before calling
const existingPlan = await getArtifactVersion(planArtifactId(sessionId), 1)
if (existingPlan) throw new Error('Plan already exists; revise it via update') Type guard
function isPlanSlotTaken(e: unknown): e is PlanSlotTakenError {
return e instanceof PlanSlotTakenError
} Try / catch
try {
await artifacts.create({ role: 'plan', sessionId, content })
} catch (e) {
if (e instanceof PlanSlotTakenError) {
await artifacts.update(e.plan.id, { content })
return
}
throw e
} Prevention
- Deduplicate model tool calls so a successful plan create is never retried
- Because the plan id is deterministic per session, prefer create-if-absent/update-if-present logic
- Carry approval state via keepApproved/approvedVersion when replacing plans
When it happens
Trigger: Calling create({ role: 'plan', sessionId, ... }) when a persisted plan artifact already exists for that session — duplicate plan creation by the model, or a race where two plan writes run concurrently.
Common situations: Model re-issuing a plan-create tool call after a retry that actually succeeded; a workflow stage resetting without clearing the old plan; the user re-running the planning step on a session that already stored a plan.
Related errors
- The session plan document may not be written here
- ArtifactPersistenceError
- AI response was empty
- AI response contained empty code block
- AI response did not contain valid code. Please try rephrasin
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/9b5de2cfb7c9a6d8.
Report an issue: GitHub.