{"record":{"id":"de8d5f5f9bd65814","repo":"different-ai/openwork","slug":"workflow-requires-read-only-capabilities","errorCode":"workflow_requires_read_only_capabilities","errorMessage":"workflow_requires_read_only_capabilities:${required.scriptPath}","messagePattern":"workflow_requires_read_only_capabilities:(.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ee/apps/den-api/src/workflows.ts","lineNumber":422,"sourceCode":"\n  const consumed = await db.select({ id: ConfigObjectVersionTable.id }).from(ConfigObjectVersionTable).where(and(\n    eq(ConfigObjectVersionTable.organizationId, resource.configObject.organizationId),\n    eq(ConfigObjectVersionTable.configObjectId, resource.configObject.id),\n    eq(ConfigObjectVersionTable.sourceRevisionRef, receipt.id),\n  )).limit(1)\n  if (consumed[0]) throw new Error(\"workflow_test_receipt_already_used\")\n\n  const built = await input.buildTools()\n  const manifestByPath = new Map(built.manifest.flatMap((entry) => [\n    [entry.scriptPath, entry] as const,\n    [entry.scriptPath.replace(/^tools\\./, \"\"), entry] as const,\n  ]))\n  for (const required of payload.parsed.requiredCapabilities) {\n    const current = manifestByPath.get(required.scriptPath)\n    if (!current || current.capabilityName !== required.capabilityName) {\n      throw new Error(`workflow_capability_unavailable:${required.scriptPath}`)\n    }\n    if (current.readOnly !== true) throw new Error(`workflow_requires_read_only_capabilities:${required.scriptPath}`)\n  }\n  for (const call of parseCodemodeToolCalls(receipt.tool_calls)) {\n    if (!payload.parsed.requiredCapabilities.some((required) => {\n      const normalized = call.name.replace(/^tools\\./, \"\")\n      return required.scriptPath === call.name || required.scriptPath.replace(/^tools\\./, \"\") === normalized\n    })) throw new Error(`workflow_test_capability_mismatch:${call.name}`)\n  }\n\n  const now = new Date()\n  const configObjectVersionId = createDenTypeId(\"configObjectVersion\")\n  await db.transaction(async (tx) => {\n    await tx.insert(ConfigObjectVersionTable).values({\n      id: configObjectVersionId,\n      organizationId: resource.configObject.organizationId,\n      configObjectId: resource.configObject.id,\n      normalizedPayloadJson: payload.value,\n      rawSourceText: input.draft.code,\n      schemaVersion: \"codemode-script-v1\",","sourceCodeStart":404,"sourceCodeEnd":440,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-api/src/workflows.ts#L404-L440","documentation":"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.","triggerScenarios":"Registering a workflow whose payload.parsed.requiredCapabilities includes a script whose manifest entry has readOnly !== true (false or undefined).","commonSituations":"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.","solutions":["Mark the required tool as readOnly: true in its plugin/capability definition and rebuild the manifest.","Remove the non-read-only capability from the workflow's requiredCapabilities and re-test.","If mutation is genuinely needed, use a workflow mechanism that permits write tools (if available) instead of the read-only path."],"exampleFix":"// before (tool definition)\nexport const tool = defineTool({ name: \"sendEmail\", readOnly: false, ... })\n// after\nexport const tool = defineTool({ name: \"sendEmail\", readOnly: true, ... }) // or drop it from the workflow","handlingStrategy":"validation","validationCode":"const built = await buildTools()\nconst notReadOnly = requiredCapabilities.filter(r => {\n  const e = built.manifest.find(m => m.scriptPath === r.scriptPath || m.scriptPath.replace(/^tools\\./, \"\") === r.scriptPath)\n  return e && e.readOnly !== true\n})\nif (notReadOnly.length) throw new Error(`not read-only: ${notReadOnly.map(m => m.scriptPath)}`)","typeGuard":"const isReadOnlyCap = (e: {readOnly?: boolean} | undefined): e is {readOnly: true} => e?.readOnly === true","tryCatchPattern":"try {\n  await createWorkflowVersion(input)\n} catch (e) {\n  if (String(e.message).startsWith(\"workflow_requires_read_only_capabilities:\")) {\n    const path = e.message.split(\":\")[1]\n    // flip readOnly: true on that tool or remove it from the workflow\n  } else throw e\n}","preventionTips":["Audit plugin tools for readOnly: true before declaring them in workflows.","Gate CI on a check that all workflow-required capabilities are read-only.","Watch plugin updates that change tool mutability."],"tags":["workflow","capability","read-only","den-api"],"backgroundTag":"capability-not-read-only","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}