Avoid duplicate skills in your org registry
A registry that grows without limits eventually grows the same skill twice.
The root cause is almost always discovery: someone builds a skill similar to one that already exists because they never knew it was there. Enable discovery and distribution from the first guide in this series is the main defense against that, and it's worth getting right before layering on anything else. But discovery will always have room to slip. This guide adds two safeguards.
We'll build this in three parts:
- Identify similar skills in the Publish Skill workflow - catch an obvious duplicate at submission time, before a pull request even opens.
- Assess skill uniqueness in the Skill Certification Review workflow - a deeper, asynchronous check that scores overlap against every other cataloged skill and links the skills involved.
- Visualize in your dashboards - so reviewers see a duplicate flag before approving a request, and platform teams see how much duplication is accumulating registry-wide.
Common use cases
- Rework instead of reuse. One team already built a skill to calculate blast radius, five more teams shouldn't have to spend the time building their own.
- No clear golden path. Once several versions of the same process exist, nothing tells a developer which one is current, which one is actually maintained, or which one is cheapest to run.
- Knowledge chaos. Skills covering the same ground tend to end up with similar descriptions. When several of them load into the same agent's context, the agent has to guess which one to call, and contradicting instructions from the near-duplicates make each of them perform worse individually.
Prerequisites
This guide assumes you have all three previous guides implemented:
- The skill blueprint from Set up a skills registry.
- The Publish Skill workflow from Ship new skills through one golden path.
- The Skill Certification Review workflow from Certify skills to meet industry and org standards.
- A Port account with permissions to edit blueprints, create workflows, and build dashboards.
Step 1: Identify similar skills pre-publish
This extends the Publish Skill workflow from Ship new skills through one golden path with an early gate: before a pull request ever opens, an AI node compares the new submission against every cataloged skill. If it finds one that covers substantially the same purpose, the publisher has to explicitly acknowledge that and confirm they still want to publish, or cancel.
The workflow now has seven nodes:
- Trigger - unchanged from the previous guide: raw
SKILL.mdcontent and an optional target path. - Check for Similar Skill - a new AI node that calls
list_entitieson theskillblueprint and compares the new submission's description against every existing skill for overlapping purpose, not just matching text. - Similar Skill Found? - a condition node that branches on whether the AI node found a match.
- Confirm Publish Despite Similar Skill - a new input node that only runs when a similar skill was found. It pauses the run and asks the publisher, specifically the same person who triggered it, to either confirm they still want to publish or cancel outright.
- Extract Skill Metadata - unchanged.
- Dispatch Skill Publish PR - unchanged.
- Create Skill Entity - unchanged.
Check for Similar Skill also uses the node's variables block to turn the AI's raw JSON response into two directly-referenceable outputs: similar_skill_identifier, and a ready-to-click similar_skill_link pointing at the existing skill's entity page. The condition node and the confirmation prompt both read these instead of re-parsing the AI's response themselves.
To update the workflow:
-
Go to the Workflows page in Port.
-
Click Edit on the Publish Skill workflow you created in the previous guide.
-
Click the
{...}button to open the JSON editor. -
Copy and paste the workflow JSON below to replace it:
Publish Skill workflow JSON, with similarity check (click to expand)
{"identifier": "publish_skill","title": "Publish Skill","icon": "Rocket","description": "Publish a new SKILL.md to the skills registry: checks for a similar existing skill, opens a pull request, and creates the corresponding skill entity as pending.","allowAnyoneToViewRuns": true,"nodes": [{"identifier": "trigger","config": {"type": "SELF_SERVE_TRIGGER","userInputs": {"properties": {"skill_content": {"title": "SKILL.md content","description": "Full raw content of the SKILL.md file to publish.","type": "string","format": "multi-line"},"target_path": {"title": "Target path in skills repo (optional)","description": "Repo-relative path, e.g. \"skills/my-new-skill/SKILL.md\". Leave empty to let the workflow choose one.","type": "string"}},"required": ["skill_content"],"order": ["skill_content", "target_path"]}}},{"identifier": "check_similar_skill","title": "Check for Similar Skill","config": {"type": "AI","systemPrompt": "You check whether a new skill submission duplicates an existing skill in the registry. You MUST call list_entities on the \"skill\" blueprint (include the \"description\" property) before answering - do not answer without first calling this tool, even if you think you already know the answer. Once you have the results, compare the new SKILL.md content (its frontmatter description) against each existing skill for semantic similarity - not just exact text matches, but overlapping purpose or duplicate functionality. If you find an existing skill that covers substantially the same purpose, set has_similar_skill to true and similar_skill_identifier to that skill's identifier. Otherwise set has_similar_skill to false and similar_skill_identifier to an empty string.","userPrompt": "New SKILL.md content:\n{{ .outputs.trigger.skill_content }}","tools": ["list_entities", "list_blueprints"],"outputSchema": {"type": "object","properties": {"has_similar_skill": { "type": "boolean" },"similar_skill_identifier": { "type": "string" }},"required": ["has_similar_skill", "similar_skill_identifier"]}},"variables": {"similar_skill_identifier": "{{ .result.response | fromjson | .similar_skill_identifier }}","similar_skill_link": "{{ if (.result.response | fromjson | .has_similar_skill) then (\"https://app.port.io/skillEntity?identifier=\" + (.result.response | fromjson | .similar_skill_identifier | gsub(\"/\"; \"%2F\"))) else \"\" end }}"}},{"identifier": "similarity_check","title": "Similar Skill Found?","config": {"type": "CONDITION","outlets": [{"identifier": "similar_skill_found","title": "Similar skill found","expression": "(.outputs.check_similar_skill.similar_skill_identifier // \"\") != \"\""}]},"links": ["{{ .outputs.check_similar_skill.similar_skill_link }}"]},{"identifier": "similar_skill_approval","title": "Confirm Publish Despite Similar Skill","config": {"type": "INPUT","description": "A similar skill may already exist in the registry: {{ .outputs.check_similar_skill.similar_skill_identifier }}\nView it here: {{ .outputs.check_similar_skill.similar_skill_link }}\n\nDo you still want to publish this new skill?","userInputs": {"properties": {},"buttons": [{ "identifier": "publish_anyway", "label": "Publish Anyway", "variant": "PRIMARY" },{ "identifier": "cancel", "label": "Cancel Publish", "variant": "DANGER" }]},"outlets": [{ "evaluationMethod": "button", "identifier": "publish_anyway", "title": "Publish Anyway", "numOfResponders": 1 },{"evaluationMethod": "button","identifier": "cancel","title": "Cancel Publish","numOfResponders": 1,"statusLabel": { "text": "Skipped - publish cancelled by requester", "variant": "alert" },"workflowStatusLabel": { "text": "Skipped - publish cancelled by requester", "variant": "alert" }}],"responders": {"users": ["{{ .workflowRun.trigger.by.email }}"]}}},{"identifier": "extract_skill_metadata","title": "Extract Skill Metadata","config": {"type": "AI","systemPrompt": "You extract metadata from SKILL.md files. Read the frontmatter (between --- lines) for the skill's name and description. If target_path is empty, call list_entities on the \"skill\" blueprint (including the \"path\" property) to see where existing skills already live, then propose a path that fits the same directory conventions instead of inventing a flat one. Always return the target_path to use, either the user's or your proposed one.","userPrompt": "SKILL.md content:\n{{ .outputs.trigger.skill_content }}\n\nUser-provided target_path (may be empty): {{ .outputs.trigger.target_path }}","tools": ["list_entities", "list_blueprints"],"outputSchema": {"type": "object","properties": {"skill_name": { "type": "string" },"description": { "type": "string" },"target_path": { "type": "string" }},"required": ["skill_name", "description", "target_path"]}}},{"identifier": "dispatch_publish_pr","title": "Dispatch Skill Publish PR","config": {"type": "INTEGRATION_ACTION","installationId": "<your-github-integration-installation-id>","integrationProvider": "github-ocean","integrationInvocationType": "dispatch_workflow","integrationActionExecutionProperties": {"org": "your-org","repo": "skills-registry","workflow": "publish-skill.yml","workflowInputs": {"skill_name": "{{ .outputs.extract_skill_metadata.response | fromjson | .skill_name }}","target_path": "{{ .outputs.extract_skill_metadata.response | fromjson | .target_path }}","skill_content": "{{ .outputs.trigger.skill_content }}","node_run_id": "{{ .workflowNodeRun.identifier }}"},"reportWorkflowStatus": true}}},{"identifier": "create_skill_entity","title": "Create Skill Entity","config": {"type": "UPSERT_ENTITY","blueprintIdentifier": "skill","mapping": {"identifier": "{{ \"your-org/skills-registry/\" + (.outputs.extract_skill_metadata.response | fromjson | .target_path) }}","title": "{{ .outputs.extract_skill_metadata.response | fromjson | .skill_name }}","properties": {"description": "{{ .outputs.extract_skill_metadata.response | fromjson | .description }}","path": "{{ .outputs.extract_skill_metadata.response | fromjson | .target_path }}","author": "{{ .workflowRun.trigger.by.email }}","pr_url": "{{ .outputs.dispatch_publish_pr.pr_url }}","publish_status": "pending"}}}}],"connections": [{ "sourceIdentifier": "trigger", "targetIdentifier": "check_similar_skill" },{ "sourceIdentifier": "check_similar_skill", "targetIdentifier": "similarity_check" },{ "sourceIdentifier": "similarity_check", "targetIdentifier": "extract_skill_metadata", "fallback": true },{ "sourceIdentifier": "similarity_check", "targetIdentifier": "similar_skill_approval", "sourceOutletIdentifier": "similar_skill_found" },{ "sourceIdentifier": "similar_skill_approval", "targetIdentifier": "extract_skill_metadata", "sourceOutletIdentifier": "publish_anyway" },{ "sourceIdentifier": "extract_skill_metadata", "targetIdentifier": "dispatch_publish_pr" },{ "sourceIdentifier": "dispatch_publish_pr", "targetIdentifier": "create_skill_entity" }]} -
The placeholders in
dispatch_publish_prandcreate_skill_entityare unchanged from the previous guide; replace them the same way if you haven't already. -
Click Save.
If the publisher clicks Cancel Publish, the run stops there: no pull request opens, and no entity gets created. If they click Publish Anyway, the run continues exactly as it did before this guide.
Step 2: Assess skill uniqueness
The check in Step 1 only catches a duplicate the publisher happens to trigger against what's cataloged at that exact moment. Certification runs the same comparison again, asynchronously, and records the result on the entity so it shows up on a dashboard regardless of what the publisher saw.
Add uniqueness to the skill blueprint
-
Go to the Data model page in Port.
-
Expand the
skillblueprint, click the...button, and select Edit blueprint. -
Click on the
{...} Edit JSONbutton. -
Add the following properties under
schema.properties:New skill blueprint properties (click to expand)
{"uniqueness": {"title": "Uniqueness","type": "string","description": "How much this skill's purpose and instructions overlap with another cataloged skill","enum": ["unique", "partial_duplicate", "mostly_duplicate", "complete_duplicate"],"enumColors": {"unique": "green","partial_duplicate": "yellow","mostly_duplicate": "orange","complete_duplicate": "red"}},"uniqueness_reasoning": {"title": "Uniqueness Reasoning","type": "string","format": "markdown","description": "Why the certification review scored this skill's uniqueness the way it did"}} -
Add the following relation under
relations:New skill blueprint relation (click to expand)
{"similar_skills": {"title": "Similar Skills","target": "skill","required": false,"many": true}} -
Click Save.
similar_skills is a relation from a skill back to itself: each skill the certification review flags as overlapping gets linked here directly, so a reviewer doesn't have to re-run the comparison by hand to see what a flagged skill actually overlaps with.
Add a uniqueness scorecard
-
Still on the
skillblueprint, click on the Scorecards tab. -
Click + Scorecard.
-
Click on the
{...} Edit JSONbutton in the top right corner. -
Paste the following JSON configuration:
Skill uniqueness scorecard (click to expand)
{"identifier": "skill_uniqueness","title": "Skill Uniqueness","blueprint": "skill","levels": [{ "title": "Basic", "color": "lightGray" },{ "title": "Bronze", "color": "bronze" },{ "title": "Silver", "color": "silver" },{ "title": "Gold", "color": "gold" }],"rules": [{"identifier": "uniqueness_not_complete_duplicate","title": "Not a complete duplicate","level": "Bronze","query": {"combinator": "and","conditions": [{ "operator": "!=", "property": "uniqueness", "value": "complete_duplicate" }]}},{"identifier": "uniqueness_unique_or_partial","title": "Unique or only partially overlapping","level": "Silver","query": {"combinator": "and","conditions": [{ "operator": "!=", "property": "uniqueness", "value": "mostly_duplicate" }]}},{"identifier": "uniqueness_is_unique","title": "Fully unique","level": "Gold","query": {"combinator": "and","conditions": [{ "operator": "!=", "property": "uniqueness", "value": "partial_duplicate" }]}}]} -
Click Save.
Each level rules out a worse overlap than the one before it: Bronze only requires that the skill isn't a complete duplicate, Silver additionally rules out a mostly-duplicate, and Gold requires no overlap being flagged at all, not even a partial one.
Extend the Skill Certification Review workflow
The Skill Certification Review workflow now has one more processing node, inserted right after the entity resolves and before the description-quality check:
- On Skill Created and Run Certification Review - unchanged.
- Resolve Skill Entity - unchanged.
- Check Uniqueness - a new AI node that reads the resolved skill's description and full content, calls
list_entitieson theskillblueprint, and compares this skill against every other cataloged skill. It returns auniquenessverdict and the identifiers of any skills it substantially overlaps with. - Check Description Quality - unchanged.
- Synthesize Recommendation - now weighs both check results. A skill that's a complete duplicate is disqualifying on its own, the same way a missing description already was.
- Update Skill Entity - now also writes
uniquenessanduniqueness_reasoning, and maps the AI's list of overlapping skill identifiers onto thesimilar_skillsrelation added above. - Notify Admin Slack - unchanged.
To update the workflow:
-
Go to the Workflows page in Port.
-
Click Edit on the Skill Certification Review workflow you created in the previous guide.
-
Click the
{...}button to open the JSON editor. -
Copy and paste the workflow JSON below to replace it:
Skill Certification Review workflow JSON, with uniqueness check (click to expand)
{"identifier": "skill_certification_review","title": "Skill Certification Review","icon": "Award","description": "Review a skill's uniqueness and description quality, and record a certification recommendation, automatically after it's created or on demand.","allowAnyoneToViewRuns": true,"nodes": [{"identifier": "trigger_event","title": "On Skill Created","config": {"type": "EVENT_TRIGGER","event": {"type": "ENTITY_CREATED","blueprintIdentifier": "skill"}}},{"identifier": "trigger_manual","title": "Run Certification Review","config": {"type": "SELF_SERVE_TRIGGER","userInputs": {"properties": {"skill": {"title": "Skill","type": "string","format": "entity","blueprint": "skill"}},"required": ["skill"]},"contexts": [{ "on": "ENTITY", "userInput": "skill" }]}},{"identifier": "resolve_skill","title": "Resolve Skill Entity","config": {"type": "WEBHOOK","method": "GET","url": "https://api.port.io/v1/blueprints/skill/entities/{{ (.outputs.trigger_event.diff.after.identifier // .outputs.trigger_manual.skill) | @uri }}","onFailure": "terminate"}},{"identifier": "check_uniqueness","title": "Check Uniqueness","config": {"type": "AI","systemPrompt": "You are reviewing a proposed skill for a skills registry certification pipeline. Judge only uniqueness relative to other cataloged skills. Call list_entities on the skill blueprint at most once, then return the structured output immediately. Do not loop. When referencing another skill, you MUST use the exact `identifier` field returned by list_entities, never the `title` field - identifiers in this catalog are full repo-relative paths (e.g. \"org/repo/path/to/SKILL.md\") and are NOT the same as the display title.","userPrompt": "Skill identifier: {{ .outputs.resolve_skill.response.entity.identifier }}\n\nDescription:\n{{ .outputs.resolve_skill.response.entity.properties.description }}\n\nFull SKILL.md content, for context:\n{{ .outputs.resolve_skill.response.entity.properties.instructions }}\n\nCompare this skill's purpose and instructions against every other cataloged skill and determine how much it overlaps with an existing one.","tools": ["list_entities", "list_blueprints"],"outputSchema": {"type": "object","properties": {"uniqueness": { "type": "string", "enum": ["unique", "partial_duplicate", "mostly_duplicate", "complete_duplicate"] },"duplicate_of": { "type": "array", "items": { "type": "string" } },"uniqueness_reasoning": { "type": "string" }},"required": ["uniqueness", "duplicate_of", "uniqueness_reasoning"]}}},{"identifier": "check_description_quality","title": "Check Description Quality","config": {"type": "AI","systemPrompt": "You review a skill's description for a registry where only the skill's name and description are preloaded into an agent's context. The description alone determines whether an agent notices the skill and calls it at the right moments. Judge the description on that standard: \"missing\" if there's no description or it says nothing about when to use the skill; \"too_vague\" if it doesn't say clearly enough when an agent should reach for this skill; \"too_verbose\" if it buries the trigger conditions in more detail than an agent needs preloaded into context; \"good\" if it's concise and makes clear both what the skill does and when to use it. Read the full SKILL.md content for context on whether the description accurately represents the skill, but judge quality by the description alone.","userPrompt": "Skill name: {{ .outputs.resolve_skill.response.entity.title }}\n\nDescription:\n{{ .outputs.resolve_skill.response.entity.properties.description }}\n\nFull SKILL.md content, for context:\n{{ .outputs.resolve_skill.response.entity.properties.instructions }}","tools": [],"outputSchema": {"type": "object","properties": {"description_quality": { "type": "string", "enum": ["good", "too_verbose", "too_vague", "missing"] },"description_quality_reasoning": { "type": "string" }},"required": ["description_quality", "description_quality_reasoning"]}}},{"identifier": "synthesize_recommendation","title": "Synthesize Recommendation","config": {"type": "AI","systemPrompt": "You turn certification checks into one recommendation for a human reviewer deciding whether to approve a pending skill. Today there are two checks, uniqueness and description quality, but this node is designed to combine more as your registry adds them. Recommend \"approve\" when every check passed, \"iterate\" when a check found something the author should fix before merging (such as a vague or verbose description, or a partial overlap with another skill), and \"reject\" only when a check found something disqualifying (such as a missing description, or a complete duplicate of an existing skill). Explain your reasoning in one or two sentences a reviewer can read at a glance.","userPrompt": "Uniqueness check result:\n{{ .outputs.check_uniqueness.response }}\n\nDescription quality check result:\n{{ .outputs.check_description_quality.response }}","tools": [],"outputSchema": {"type": "object","properties": {"certification_recommendation": { "type": "string", "enum": ["approve", "reject", "iterate"] },"certification_recommendation_reasoning": { "type": "string" }},"required": ["certification_recommendation", "certification_recommendation_reasoning"]}}},{"identifier": "update_skill_entity","title": "Update Skill Entity","config": {"type": "UPSERT_ENTITY","blueprintIdentifier": "skill","mapping": {"identifier": "{{ .outputs.trigger_event.diff.after.identifier // .outputs.trigger_manual.skill }}","properties": {"uniqueness": "{{ .outputs.check_uniqueness.response | fromjson | .uniqueness }}","uniqueness_reasoning": "{{ .outputs.check_uniqueness.response | fromjson | .uniqueness_reasoning }}","description_quality": "{{ .outputs.check_description_quality.response | fromjson | .description_quality }}","description_quality_reasoning": "{{ .outputs.check_description_quality.response | fromjson | .description_quality_reasoning }}","last_certification_check_at": "{{ now | todateiso8601 }}","certification_recommendation": "{{ .outputs.synthesize_recommendation.response | fromjson | .certification_recommendation }}","certification_recommendation_reasoning": "{{ .outputs.synthesize_recommendation.response | fromjson | .certification_recommendation_reasoning }}"},"relations": {"similar_skills": "{{ .outputs.check_uniqueness.response | fromjson | .duplicate_of }}"}},"onFailure": "terminate"}},{"identifier": "notify_admin_slack","title": "Notify Admin Slack","config": {"type": "WEBHOOK","url": "https://slack.com/api/chat.postMessage","method": "POST","headers": {"Content-Type": "application/json; charset=utf-8","Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_<team_id>\"] }}"},"body": {"channel": "<your-reviewers-channel-id>","blocks": [{"type": "section","text": {"type": "mrkdwn","text": "*{{ .outputs.resolve_skill.response.entity.title }}* certification review: *{{ .outputs.synthesize_recommendation.response | fromjson | .certification_recommendation }}*\n{{ .outputs.synthesize_recommendation.response | fromjson | .certification_recommendation_reasoning }}"}}]},"onTimeout": "continue","onFailure": "continue"}}],"connections": [{ "sourceIdentifier": "trigger_event", "targetIdentifier": "resolve_skill" },{ "sourceIdentifier": "trigger_manual", "targetIdentifier": "resolve_skill" },{ "sourceIdentifier": "resolve_skill", "targetIdentifier": "check_uniqueness" },{ "sourceIdentifier": "check_uniqueness", "targetIdentifier": "check_description_quality" },{ "sourceIdentifier": "check_description_quality", "targetIdentifier": "synthesize_recommendation" },{ "sourceIdentifier": "synthesize_recommendation", "targetIdentifier": "update_skill_entity" },{ "sourceIdentifier": "update_skill_entity", "targetIdentifier": "notify_admin_slack" }]} -
Replace
<your-reviewers-channel-id>and__SLACK_APP_BOT_TOKEN_<team_id>the same way you did in the previous guide, if you haven't already. -
Click Save.
Step 3: Visualize in your dashboards
Add uniqueness to the Skills Lifecycle Control dashboard
If you already have the Pending Publish Requests table from the previous two guides, extend it:
- Open the table widget and edit it.
- Add the Skill uniqueness scorecard as a column, alongside Production readiness and Skill discoverability.
- Click Save.
A reviewer now sees a duplicate flag right in the same table as the request itself, before they approve or reject it.
Add uniqueness to the Skills Registry Health dashboard
Add one more widget to the Skills Registry Health dashboard, alongside Skills Production Readiness, Published Skills, Not Certified, and Discoverability:
- Uniqueness:
-
Click + Widget and select Pie Chart.
-
Title:
Uniqueness. -
Description:
Checks whether a skill duplicates or overlaps with another. -
Choose the
skillblueprint. -
Under Breakdown by property, select Uniqueness.
-
Add this filter to the Initial filters editor, so the chart only counts skills that have actually been reviewed:
{"combinator": "and","rules": [{ "operator": "isNotEmpty", "property": "last_certification_check_at" }]} -
Click Save.
-
Between the extended table and this pie chart, a reviewer sees a duplicate flag before approving a request, and a platform team sees how much duplication is accumulating across the whole registry, the same way they already track description quality and production readiness.
Possible enhancements
Suggest deduplication automatically
Flagging a duplicate still leaves someone to decide what to do about it: delete one skill, or merge two into one. That decision needs the same context a human reviewer would use, plus someone to actually act on it. A dedicated workflow can propose that action instead of leaving it to whoever happens to notice a low uniqueness score.
At a high level:
- Add a
skills_dedup_suggestionblueprint. Conceptually, it needs: astatus(pending,approved,rejected), the assessment and reasoning behind it, a recommended action (for example merge or remove), a link to the pull request once one exists, and relations back to the skill entities involved, the same skill and itssimilar_skillsfrom Step 2. - Add a workflow triggered when a skill's uniqueness drops. An event trigger watching for
uniquenessbecomingmostly_duplicateorcomplete_duplicatefits the same pattern as On Skill Created above. It fetches the skill and every entity in itssimilar_skillsrelation, has an AI node assess whether one should simply be removed or whether several should be merged into one, and delegates the actual file changes to a coding agent that opens a pull request to the skills repo with the deduplicated result. The workflow records its assessment and the pull request link on a newskills_dedup_suggestionentity. - Extend the Skills Lifecycle Control dashboard. A table widget showing open
skills_dedup_suggestionentities gives reviewers the same kind of queue as Pending Publish Requests. Itspending/approved/rejectedstatuses can be automated the same way, through dedicated approve and reject workflows like the ones in the previous guide's enhancements.
Related pages
- Set up a skills registry - the discovery and distribution step this guide's safeguards back up.
- Ship new skills through one golden path - the Publish Skill workflow this guide extends in Step 1.
- Certify skills to meet industry and org standards - the Skill Certification Review workflow and dashboards this guide extends in Steps 2 and 3.
- AI node - configuring
systemPrompt,tools, andoutputSchema. - Condition node - branching a workflow on an expression.
- Input node - pausing a workflow for a person to respond.