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

  1. Log the raw POST /v1/plugins response body and check whether item.id exists or the shape differs (e.g. plugin at top level)
  2. Verify the plugin was actually created by listing plugins afterward; if it exists, recover the id from the list instead of re-creating
  3. Align client and server versions — schema drift on the create-plugin response is the usual cause
  4. Check that no proxy/mock/interceptor strips or wraps the response body
  5. 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

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


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