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.

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.

Appendix: 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)