different-ai/openwork · error

workflow_requires_read_only_capabilities

workflow_requires_read_only_capabilities

Error message

workflow_requires_read_only_capabilities:${required.scriptPath}

What it means

Thrown in createWorkflowVersion when a required capability exists in the manifest but its readOnly flag is not exactly true. Workflows may only depend on read-only capabilities, so any required tool that can mutate state blocks version registration.

Source

Thrown at ee/apps/den-api/src/workflows.ts:422

  const consumed = await db.select({ id: ConfigObjectVersionTable.id }).from(ConfigObjectVersionTable).where(and(
    eq(ConfigObjectVersionTable.organizationId, resource.configObject.organizationId),
    eq(ConfigObjectVersionTable.configObjectId, resource.configObject.id),
    eq(ConfigObjectVersionTable.sourceRevisionRef, receipt.id),
  )).limit(1)
  if (consumed[0]) throw new Error("workflow_test_receipt_already_used")

  const built = await input.buildTools()
  const manifestByPath = new Map(built.manifest.flatMap((entry) => [
    [entry.scriptPath, entry] as const,
    [entry.scriptPath.replace(/^tools\./, ""), entry] as const,
  ]))
  for (const required of payload.parsed.requiredCapabilities) {
    const current = manifestByPath.get(required.scriptPath)
    if (!current || current.capabilityName !== required.capabilityName) {
      throw new Error(`workflow_capability_unavailable:${required.scriptPath}`)
    }
    if (current.readOnly !== true) throw new Error(`workflow_requires_read_only_capabilities:${required.scriptPath}`)
  }
  for (const call of parseCodemodeToolCalls(receipt.tool_calls)) {
    if (!payload.parsed.requiredCapabilities.some((required) => {
      const normalized = call.name.replace(/^tools\./, "")
      return required.scriptPath === call.name || required.scriptPath.replace(/^tools\./, "") === normalized
    })) throw new Error(`workflow_test_capability_mismatch:${call.name}`)
  }

  const now = new Date()
  const configObjectVersionId = createDenTypeId("configObjectVersion")
  await db.transaction(async (tx) => {
    await tx.insert(ConfigObjectVersionTable).values({
      id: configObjectVersionId,
      organizationId: resource.configObject.organizationId,
      configObjectId: resource.configObject.id,
      normalizedPayloadJson: payload.value,
      rawSourceText: input.draft.code,
      schemaVersion: "codemode-script-v1",

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Mark the required tool as readOnly: true in its plugin/capability definition and rebuild the manifest.
  2. Remove the non-read-only capability from the workflow's requiredCapabilities and re-test.
  3. If mutation is genuinely needed, use a workflow mechanism that permits write tools (if available) instead of the read-only path.

Example fix

// before (tool definition)
export const tool = defineTool({ name: "sendEmail", readOnly: false, ... })
// after
export const tool = defineTool({ name: "sendEmail", readOnly: true, ... }) // or drop it from the workflow
Defensive patterns

Strategy: validation

Validate before calling

const built = await buildTools()
const notReadOnly = requiredCapabilities.filter(r => {
  const e = built.manifest.find(m => m.scriptPath === r.scriptPath || m.scriptPath.replace(/^tools\./, "") === r.scriptPath)
  return e && e.readOnly !== true
})
if (notReadOnly.length) throw new Error(`not read-only: ${notReadOnly.map(m => m.scriptPath)}`)

Type guard

const isReadOnlyCap = (e: {readOnly?: boolean} | undefined): e is {readOnly: true} => e?.readOnly === true

Try / catch

try {
  await createWorkflowVersion(input)
} catch (e) {
  if (String(e.message).startsWith("workflow_requires_read_only_capabilities:")) {
    const path = e.message.split(":")[1]
    // flip readOnly: true on that tool or remove it from the workflow
  } else throw e
}

Prevention

When it happens

Trigger: Registering a workflow whose payload.parsed.requiredCapabilities includes a script whose manifest entry has readOnly !== true (false or undefined).

Common situations: A capability author changed a tool to be mutating after the workflow was authored; a plugin update flipped readOnly off; the workflow was originally validated against a read-only variant that later became write-capable.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/de8d5f5f9bd65814. Report an issue: GitHub.