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.

Solutions

  1. Catch PlanSlotTakenError and use error.plan (or planArtifactId(sessionId)) to update/revise the existing plan instead of creating one
  2. Read the existing plan first (getVersion/getArtifactVersion) and only create when none exists
  3. 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

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


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/9b5de2cfb7c9a6d8. Report an issue: GitHub.

Appendix: 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)