different-ai/openwork · error · DenApiError
invalid_plugin_payload
invalid_plugin_payload
Error message
Plugin was created but no id was returned.
What it means
DenApiError thrown by createOrgPlugin in apps/app/src/app/lib/den.ts when POST /v1/plugins returned success but the response did not contain payload.item.id as a string — i.e. the plugin was created server-side yet the client could not extract its id. The message explicitly flags the awkward state that the resource exists but its identifier is unusable.
Source
Thrown at apps/app/src/app/lib/den.ts:3501
baseUrls,
"/v1/plugins",
{
method: "POST",
token,
organizationId: orgId,
body: {
name: input.name,
description: input.description ?? null,
components: input.components,
orgWide: input.orgWide === true,
...(marketplaceId ? { marketplaceId } : {}),
},
},
);
const item = isRecord(payload) && isRecord(payload.item) ? payload.item : null;
const pluginId = item && typeof item.id === "string" ? item.id : null;
if (!pluginId) {
throw new DenApiError(500, "invalid_plugin_payload", "Plugin was created but no id was returned.");
}
return pluginId;
},
async getBillingStatus(options: { includePortal?: boolean; includeInvoices?: boolean } = {}): Promise<DenBillingSummary> {
const params = new URLSearchParams();
if (options.includePortal === false) {
params.set("excludePortal", "1");
}
if (options.includeInvoices === false) {
params.set("excludeInvoices", "1");
}
const path = params.size > 0 ? `/v1/workers/billing?${params.toString()}` : "/v1/workers/billing";
const payload = await requestJson<unknown>(baseUrls, path, {
method: "GET",
token,
});View on GitHub (pinned to 2b7df46e8a)
Solutions
- Log the raw POST /v1/plugins response body and check whether item.id exists or the shape differs (e.g. plugin at top level)
- Verify the plugin was actually created by listing plugins afterward; if it exists, recover the id from the list instead of re-creating
- Align client and server versions — schema drift on the create-plugin response is the usual cause
- Check that no proxy/mock/interceptor strips or wraps the response body
- Retry once only after confirming the plugin was not created, to avoid duplicate plugins
Example fix
// before
const item = isRecord(payload) && isRecord(payload.item) ? payload.item : null;
const pluginId = item && typeof item.id === "string" ? item.id : null;
if (!pluginId) {
throw new DenApiError(500, "invalid_plugin_payload", "Plugin was created but no id was returned.");
}
// after
caller-side handling of the created-but-no-id state:
try {
const id = await client.createOrgPlugin(orgId, input);
} catch (err) {
if (err instanceof DenApiError && err.code === "invalid_plugin_payload") {
const plugins = await client.listOrgPlugins(orgId); // reconcile instead of blind retry
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// validate input before POST /v1/plugins
if (!input.name || input.name.trim().length === 0) {
throw new Error("Plugin name is required before creation");
}
if (!Array.isArray(input.components) || input.components.length === 0) {
throw new Error("At least one component is required");
} Type guard
function hasCreatedPluginId(v: unknown): v is { item: { id: string } } {
return (
typeof v === "object" && v !== null && "item" in v &&
typeof (v as { item?: unknown }).item === "object" && (v as { item?: unknown }).item !== null &&
typeof ((v as { item: { id?: unknown } }).item).id === "string"
);
} Try / catch
try {
const pluginId = await client.createOrgPlugin(orgId, input);
} catch (err) {
if (err instanceof DenApiError && err.code === "invalid_plugin_payload") {
// plugin may exist: reconcile via list, never blind-retry the POST
const plugins = await client.listOrgPlugins(orgId);
} else { throw err; }
} Prevention
- Never retry createOrgPlugin on this error without first checking whether the plugin was created
- Confirm the server's create-plugin response shape (item.id) matches the client version
- Log the raw POST response during development
- Guard against proxies/mocks intercepting POST /v1/plugins
- Catch on error code invalid_plugin_payload and reconcile rather than surfacing a generic failure
When it happens
Trigger: POST /v1/plugins returns 200/201 with a body that is not an object, lacks an `item` object, or whose item.id is missing/non-string (e.g. numeric id, an error envelope with 200, or an older server returning the plugin at the top level instead of nested under item).
Common situations: Server version that nests or names the created resource differently than this client expects; an intermediary (proxy, service worker, mock) swallowing the body; duplicate-name handling on the server returning a 200 status with a message payload instead of the created item; a self-hosted Den deployment drifting from the client's schema.
Related errors
- invalid_mcp_connection_payload
- invalid_marketplace_payload
- invalid_billing_payload
- extensions.add_unauthorized
- workflow_test_receipt_already_used
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/7bad0c808a1851cd.
Report an issue: GitHub.