different-ai/openwork · error

That skill is not part of this plugin.

Error message

That skill is not part of this plugin.

What it means

useSkill fetches a skill's detail from the Den API and verifies via a membership lookup that the skill actually belongs to the requested plugin (entry.pluginId matches and removedAt === null). If the plugin-membership items array does not contain such an entry, the hook throws 'That skill is not part of this plugin.' to prevent rendering skills detached from their plugin context.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx:103

    queryFn: async (): Promise<DenSkill> => {
      const encodedSkillId = encodeURIComponent(skillId);
      const [{ response, payload }, membershipResult] = await Promise.all([
        requestJson(`/v1/config-objects/${encodedSkillId}`, { method: "GET" }, 15000),
        requestJson(`/v1/config-objects/${encodedSkillId}/plugins`, { method: "GET" }, 15000),
      ]);
      if (!response.ok) {
        throw new Error(getErrorMessage(payload, `Failed to load skill (${response.status}).`));
      }
      if (!membershipResult.response.ok) {
        throw new Error(getErrorMessage(membershipResult.payload, `Failed to verify skill plugin (${membershipResult.response.status}).`));
      }
      const belongsToPlugin = isRecord(membershipResult.payload)
        && Array.isArray(membershipResult.payload.items)
        && membershipResult.payload.items.some((entry) => (
          isRecord(entry) && entry.pluginId === pluginId && entry.removedAt === null
        ));
      if (!belongsToPlugin) {
        throw new Error("That skill is not part of this plugin.");
      }
      const skill = parseSkillResponse(payload);
      if (!skill) {
        throw new Error("Skill detail response was incomplete.");
      }
      return skill;
    },
  });
}

export function useCreateSkill(pluginId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (draft: SkillDraft): Promise<DenSkill> => {
      const { response, payload } = await requestJson(
        "/v1/config-objects",
        {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Verify the pluginId argument matches the plugin that actually contains the skill (check plugin dashboard/route params).
  2. Re-install or re-attach the skill to the plugin so a membership row with removedAt === null exists.
  3. Refetch after fixing: invalidate the query / reload so a stale membership response isn't reused.
  4. If the skill was intentionally removed, update the UI entry point (link/menu) to stop requesting it under this plugin.

Example fix

// before
useSkill(routePluginId, skillId) // routePluginId from an outdated URL
// after
const pluginId = plugin?.id; // resolved from current plugin record
if (!pluginId) return null;
return useSkill(pluginId, skillId);
Defensive patterns

Strategy: type-guard

Validate before calling

function isPluginMembershipPayload(v: unknown): v is { items: { pluginId: unknown; removedAt: unknown }[] } {
  return isRecord(v) && Array.isArray(v.items) && v.items.every((e) => isRecord(e));
}

Type guard

function membershipHasSkill(items: unknown[], pluginId: string): boolean {
  return items.some((e) => isRecord(e) && e.pluginId === pluginId && e.removedAt === null);
}

Try / catch

try {
  const skill = await refetchSkill(pluginId, skillId);
} catch (e) {
  if (e instanceof Error && e.message === "That skill is not part of this plugin.") {
    // fall back to plugin skill list or show 'skill removed' state
  } else throw e;
}

Prevention

When it happens

Trigger: Calling useSkill(pluginId, skillId) where the GET membership payload's items array contains no record with entry.pluginId === pluginId and entry.removedAt === null — e.g. the skill was removed from the plugin, the wrong pluginId is passed, or the membership endpoint returns an empty/unrelated items list.

Common situations: A skill was detached or removed from a plugin (removedAt set) but a cached dashboard link still points at the old plugin; a copy/paste or route bug passes a different plugin's ID; the membership list hasn't propagated after just installing the skill.

Related errors


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