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
- Verify the pluginId exists, is active, not soft-deleted, and belongs to input.organizationId before saving
- Omit workflow.pluginId to save into the caller's default workflows plugin (auto-created if missing)
- Refresh the client's plugin list to replace the stale id
- 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
- Refresh plugin selections after any delete/deactivate operation
- Never carry pluginIds across organizations or environments
- Prefer omitting pluginId (default workflows plugin) unless targeting a shared plugin explicitly
- Handle stale-id errors by re-fetching the plugin list instead of retrying blindly
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
- invalid_plugin_payload
- extensions.add_unauthorized
- workflow_snapshot_not_found
- automation_saved_script_version_not_found
- saved_workflow_plugin_context_required
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/170f21a61aab365b.
Report an issue: GitHub.