different-ai/openwork · error

saved_workflow_manager_context_required

saved_workflow_manager_context_required

Error message

saved_workflow_manager_context_required

What it means

Saving a workflow under a name that already has a linked active config object creates a new immutable version of that workflow instead of a new workflow. Because replacing another manager's executable code must never happen implicitly, saveWorkflow requires an actor context (input.context) to check the caller holds the 'manager' role on the existing config object; without any context it throws 'saved_workflow_manager_context_required'.

Source

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

      }).where(eq(PluginTable.id, plugin.id))
    }

    const linked = await tx.select({ object: ConfigObjectTable }).from(PluginConfigObjectTable)
      .innerJoin(ConfigObjectTable, eq(ConfigObjectTable.id, PluginConfigObjectTable.configObjectId))
      .where(and(
        eq(PluginConfigObjectTable.pluginId, pluginId),
        isNull(PluginConfigObjectTable.removedAt),
        eq(ConfigObjectTable.title, input.workflow.name),
        inArray(ConfigObjectTable.objectType, ["script", "workflow"]),
        eq(ConfigObjectTable.status, "active"),
        isNull(ConfigObjectTable.deletedAt),
      )).limit(1).for("update")
    const configObjectId = linked[0]?.object.id ?? createDenTypeId("configObject")
    if (linked[0]) {
      // Saving the same name creates a new immutable version of the existing
      // Workflow. Plugin edit access can widen the audience, but it must never
      // grant authority to replace another Workflow manager's executable code.
      if (!input.context) throw new Error("saved_workflow_manager_context_required")
      await requirePluginArchResourceRole({
        context: input.context,
        resourceId: configObjectId,
        resourceKind: "config_object",
        role: "manager",
      })
    } else {
      await tx.insert(ConfigObjectTable).values({
        id: configObjectId,
        organizationId,
        objectType: "workflow",
        sourceMode: "cloud",
        title: input.workflow.name,
        description: input.workflow.description?.trim() || null,
        searchText: `${input.workflow.name} ${input.workflow.description ?? ""}`.trim(),
        currentFileName: `${input.workflow.name}.js`,
        currentFileExtension: "js",
        status: "active",

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Pass an actor context whose member holds the manager role on the existing config object
  2. Rename the workflow to a non-colliding title so a brand-new config object is created instead
  3. If you should not manage the existing workflow, choose a distinct name rather than overwriting
  4. For programmatic callers, load the PluginArchActorContext for the owning member before saving

Example fix

// before
await saveWorkflow({ organizationId, ownerMemberId, workflow: { name: existingName, ... }, buildTools }) // contextless overwrite
// after
await saveWorkflow({ organizationId, ownerMemberId, workflow: { name: 'my-report-v2', ... }, buildTools, context: actorContextWithManagerRole })
Defensive patterns

Strategy: validation

Validate before calling

async function collidesWithExistingWorkflow(name: string, pluginId: string) {
  const rows = await db.select({ id: ConfigObjectTable.id }).from(PluginConfigObjectTable)
    .innerJoin(ConfigObjectTable, eq(ConfigObjectTable.id, PluginConfigObjectTable.configObjectId))
    .where(and(
      eq(PluginConfigObjectTable.pluginId, pluginId),
      isNull(PluginConfigObjectTable.removedAt),
      eq(ConfigObjectTable.title, name),
      inArray(ConfigObjectTable.objectType, ['script', 'workflow']),
      eq(ConfigObjectTable.status, 'active'),
      isNull(ConfigObjectTable.deletedAt),
    )).limit(1)
  return rows.length > 0 // if true, an actor context with the manager role is required
}

Type guard

null

Try / catch

try {
  await saveWorkflow(input)
} catch (err) {
  if (err instanceof Error && err.message === 'saved_workflow_manager_context_required') {
    console.error('Name collides with an existing workflow — supply a manager-role context or pick a new name')
  }
}

Prevention

When it happens

Trigger: Calling saveWorkflow without input.context while input.workflow.name matches the title of an existing active script/workflow config object linked to the (resolved) plugin — the overwrite-versioning path at line 717.

Common situations: An unauthenticated/system caller re-saving a workflow whose name collides with an existing one; renaming a workflow to a name another manager already uses; background jobs omitting context that worked before only because names were unique.

Related errors


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