{"record":{"id":"273412279ca2d9a7","repo":"different-ai/openwork","slug":"saved-workflow-plugin-context-required","errorCode":"saved_workflow_plugin_context_required","errorMessage":"saved_workflow_plugin_context_required","messagePattern":"saved_workflow_plugin_context_required","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ee/apps/den-api/src/workflows.ts","lineNumber":603,"sourceCode":"export async function saveWorkflow(input: {\n  organizationId: string\n  ownerMemberId: string\n  workflow: SaveWorkflowInput\n  buildTools: () => Promise<BuiltCodemodeTools>\n  context?: PluginArchActorContext\n}): Promise<{ pluginId: string; configObjectId: string; configObjectVersionId: string }> {\n  const organizationId = normalizeDenTypeId(\"organization\", input.organizationId)\n  const ownerMemberId = normalizeDenTypeId(\"member\", input.ownerMemberId)\n  const requestedPluginId = input.workflow.pluginId\n    ? normalizeDenTypeId(\"plugin\", input.workflow.pluginId)\n    : null\n  if (requestedPluginId) {\n    if (\n      !input.context\n      || input.context.organizationContext.organization.id !== organizationId\n      || input.context.organizationContext.currentMember.id !== ownerMemberId\n    ) {\n      throw new Error(\"saved_workflow_plugin_context_required\")\n    }\n    await requirePluginArchResourceRole({\n      context: input.context,\n      resourceId: requestedPluginId,\n      resourceKind: \"plugin\",\n      role: \"editor\",\n    })\n  }\n  const receipts = await db.select().from(WorkflowRunTable).where(and(\n    eq(WorkflowRunTable.organization_id, organizationId),\n    eq(WorkflowRunTable.org_membership_id, ownerMemberId),\n    eq(WorkflowRunTable.code_digest, codemodeCodeDigest(input.workflow.code)),\n    eq(WorkflowRunTable.status, \"succeeded\"),\n    gt(WorkflowRunTable.finished_at, new Date(Date.now() - RECENT_RUN_WINDOW_MS)),\n  )).orderBy(desc(WorkflowRunTable.finished_at)).limit(1)\n  const receipt = receipts[0]\n  if (!receipt) throw new Error(\"workflow_recent_receipt_required\")\n","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-api/src/workflows.ts#L585-L621","documentation":"saveWorkflow in the Den API enforces that when a workflow is saved under an explicit pluginId, the caller must supply a PluginArchActorContext whose organization and current member exactly match the organizationId and ownerMemberId arguments. The library throws 'saved_workflow_plugin_context_required' when input.context is missing or its organization/member identity diverges, because plugin-level authorization (requirePluginArchResourceRole with role 'editor') cannot be performed without a matching actor context.","triggerScenarios":"Calling saveWorkflow with workflow.pluginId set while (a) input.context is undefined, (b) input.context.organizationContext.organization.id differs from input.organizationId, or (c) input.context.organizationContext.currentMember.id differs from input.ownerMemberId.","commonSituations":"Server-side/cron callers saving workflows on behalf of a member without loading the actor context; passing a context from a different organization after an org switch; reusing a stale member id after the owner membership changed; internal scripts that omit context for the fast path but also set a pluginId.","solutions":["Always pass a PluginArchActorContext when workflow.pluginId is provided","Ensure input.context.organizationContext.organization.id equals the organizationId argument","Ensure input.context.organizationContext.currentMember.id equals the ownerMemberId argument","If saving to the member's default workflows plugin, omit workflow.pluginId so no context is required"],"exampleFix":"// before\nawait saveWorkflow({ organizationId, ownerMemberId, workflow: { pluginId: somePluginId, ... }, buildTools })\n// after\nawait saveWorkflow({ organizationId, ownerMemberId, workflow: { pluginId: somePluginId, ... }, buildTools, context: actorContext // must match organizationId & ownerMemberId\n})","handlingStrategy":"validation","validationCode":"function canSaveToPlugin(input) {\n  return !input.workflow.pluginId || Boolean(\n    input.context\n    && input.context.organizationContext.organization.id === input.organizationId\n    && input.context.organizationContext.currentMember.id === input.ownerMemberId\n  )\n}","typeGuard":"function hasMatchingActorContext(input: Parameters<typeof saveWorkflow>[0]): input is typeof input & { context: PluginArchActorContext } {\n  return input.context != null\n    && input.context.organizationContext.organization.id === input.organizationId\n    && input.context.organizationContext.currentMember.id === input.ownerMemberId\n}","tryCatchPattern":"try {\n  await saveWorkflow(input)\n} catch (err) {\n  if (err instanceof Error && err.message === 'saved_workflow_plugin_context_required') {\n    throw new Error('Provide an actor context matching organizationId/ownerMemberId when pluginId is set')\n  }\n  throw err\n}","preventionTips":["Never set workflow.pluginId without also supplying input.context","Assert context identity (org + member) equals the save target in a pre-flight check","Centralize saveWorkflow calls in one helper that always loads a fresh actor context","Write a test asserting the no-context + pluginId combination fails fast"],"tags":["authorization","den-api","workflows","context"],"backgroundTag":"missing-authorization-context","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}