windmill-labs/windmill · error · PlanWriteRefusedError
The session plan document may not be written here
Error message
The session plan document may not be written here
What it means
PlanWriteRefusedError is thrown by artifactsState.create when a plan-role artifact write is attempted while the caller-provided canWritePlan() policy returns false. It distinguishes 'the plan is read-only in this posture' from 'no such artifact' (undefined), so callers cannot misread a refusal as a missing document. Tool wrappers report it back to the model as a refused plan write.
Source
Thrown at frontend/src/lib/components/copilot/chat/artifacts/artifactsState.svelte.ts:175
sessionId,
chatId: input.chatId,
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(View on GitHub (pinned to e474e8803c)
Solutions
- Check canWritePlan() (or the session's plan posture) before issuing the plan-create tool call
- If a plan already exists and is locked, revise the existing plan artifact (update) instead of creating a new one
- Move the session back to a writable planning posture if the workflow intends a new plan
Example fix
// before
await artifacts.create({ role: 'plan', sessionId, content }) // throws if refused
// after
if (artifacts.canWritePlan()) await artifacts.create({ role: 'plan', sessionId, content })
else await artifacts.update(planArtifactId(sessionId), { content }) Defensive patterns
Strategy: type-guard
Validate before calling
if (!canWritePlan()) {
throw new Error('Plan writes are not permitted in the current posture')
} Type guard
function isPlanWriteRefused(e: unknown): e is PlanWriteRefusedError {
return e instanceof PlanWriteRefusedError
} Try / catch
try {
await artifacts.create({ role: 'plan', sessionId, content })
} catch (e) {
if (e instanceof PlanWriteRefusedError) {
toolCallbacks.report('Plan is read-only here; revise the existing plan instead')
return
}
throw e
} Prevention
- Gate plan-create tool availability on the session's planning posture
- Use update on the existing plan artifact once planning is locked
- Report PlanWriteRefusedError to the model as a refusal, not a missing-file error
When it happens
Trigger: Calling create({ role: 'plan', sessionId, ... }) when opts.canWritePlan() is false — e.g. the plan was already approved/locked, the chat is in a read-only review posture, or plan writing is disabled for the current tool context.
Common situations: Model trying to mint a second plan after approval; a tool invoking plan creation outside the phase where plan writes are permitted; replaying an old plan-create tool call against a session that has moved past the planning stage.
Related errors
- PlanSlotTakenError
- 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/df48a93dd90a180d.
Report an issue: GitHub.