different-ai/openwork · error

saved_workflow_plugin_not_found

saved_workflow_plugin_not_found

Error message

saved_workflow_plugin_not_found

What it means

When saving under an explicit pluginId, saveWorkflow looks up the plugin row (id, same organization, active, not deleted, locked FOR UPDATE). If requestedPluginId was supplied but no matching active plugin row exists, it throws 'saved_workflow_plugin_not_found'. Legacy-name fallback only applies when no explicit pluginId was requested.

Source

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

        )).limit(1).for("update")
      : await tx.select().from(PluginTable).where(and(
          eq(PluginTable.organizationId, organizationId),
          eq(PluginTable.createdByOrgMembershipId, ownerMemberId),
          eq(PluginTable.name, DEFAULT_WORKFLOWS_PLUGIN_NAME),
          eq(PluginTable.status, "active"),
          isNull(PluginTable.deletedAt),
        )).limit(1).for("update")
    const legacyPlugins = requestedPluginId || plugins[0]
      ? []
      : await tx.select().from(PluginTable).where(and(
          eq(PluginTable.organizationId, organizationId),
          eq(PluginTable.createdByOrgMembershipId, ownerMemberId),
          inArray(PluginTable.name, LEGACY_WORKFLOW_PLUGIN_NAMES),
          eq(PluginTable.status, "active"),
          isNull(PluginTable.deletedAt),
        )).limit(1).for("update")
    const plugin = plugins[0] ?? legacyPlugins[0]
    if (requestedPluginId && !plugin) throw new Error("saved_workflow_plugin_not_found")
    const pluginId = plugin?.id ?? createDenTypeId("plugin")
    if (!plugin) {
      await tx.insert(PluginTable).values({
        id: pluginId,
        organizationId,
        name: DEFAULT_WORKFLOWS_PLUGIN_NAME,
        description: "Private reusable Workflows.",
        status: "active",
        createdByOrgMembershipId: ownerMemberId,
      })
      await tx.insert(PluginAccessGrantTable).values({
        id: createDenTypeId("pluginAccessGrant"),
        organizationId,
        pluginId,
        orgMembershipId: ownerMemberId,
        teamId: null,
        orgWide: false,
        role: "manager",

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Verify the pluginId exists, is active, not soft-deleted, and belongs to input.organizationId before saving
  2. Omit workflow.pluginId to save into the caller's default workflows plugin (auto-created if missing)
  3. Refresh the client's plugin list to replace the stale id
  4. Re-create the plugin if it was deleted and use its new id

Example fix

// before
const [plugin] = await db.select().from(PluginTable).where(eq(PluginTable.id, staleId))
await saveWorkflow({ workflow: { pluginId: staleId, ... } }) // throws if stale
// after
const [plugin] = await db.select().from(PluginTable).where(and(eq(PluginTable.id, staleId), eq(PluginTable.organizationId, organizationId), eq(PluginTable.status, 'active'), isNull(PluginTable.deletedAt)))
if (!plugin) throw new Error('re-create or re-select the plugin first')
await saveWorkflow({ workflow: { pluginId: plugin.id, ... } })
Defensive patterns

Strategy: validation

Validate before calling

async function pluginIsSaveable(pluginId: string, organizationId: string) {
  const [p] = await db.select().from(PluginTable).where(and(
    eq(PluginTable.id, pluginId),
    eq(PluginTable.organizationId, organizationId),
    eq(PluginTable.status, 'active'),
    isNull(PluginTable.deletedAt),
  )).limit(1)
  return p != null
}

Type guard

null

Try / catch

try {
  await saveWorkflow(input)
} catch (err) {
  if (err instanceof Error && err.message === 'saved_workflow_plugin_not_found') {
    console.error('pluginId is missing/inactive/deleted or belongs to another org — refresh and reselect')
  }
}

Prevention

When it happens

Trigger: Calling saveWorkflow with workflow.pluginId set to a plugin id that does not exist, belongs to a different organization, has status != 'active', or was soft-deleted (deletedAt set).

Common situations: Stale pluginId cached in a client after the plugin was deleted; copying a workflow/pluginId from another org into this one; a race where the plugin is deleted between listing and saving; passing an id from a test environment into production.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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