ProductOSDocs

Entity reference · v1

external-links

table feature_external_links

Links from a feature (Work item) to things outside ProductOS: GitHub pull requests, issues and branches, Claude Code sessions, Notion pages, Figma files and pinned repo paths. The same feature and url, or the same feature, kind and external_id, is one link. Written only by attach_artifact and the attach_external_link, update_external_link and detach_external_link actions, never by POST, PATCH or DELETE here.

An external link ties a feature (a Work item) to something outside ProductOS: a GitHub pull request, issue, commit or branch, a Claude Code session, a Notion page, a Figma file, or a pinned repo path. The feature dialog shows them in its Documentation, Design and Code groups.

A link is the same link when it has the same feature and url, or the same feature, kind and external_id. Two unique indexes hold that line, and every way of attaching a link returns the row already there instead of adding a second one:

  • the MCP attach_artifact tool and the catalog's attach_external_link action, which answer with existing: true;
  • update_session and start_session when they attach a session's pull request or the session itself;
  • POST /api/v1/features/{id}/documents, which answers 200 with existing: true rather than 201;
  • the workbook import, which skips a link the feature already has.

One pull request can still be on two Work items: each feature holds its own link to it.

external_id

Kindexternal_id
github_pr, github_issueowner/repo#number, such as ProductOS-Inc/ProductOS#123. Taken from the url when you leave it out; a bare 123 is completed from the url; an id that names a different pull request than the url is refused.
github_commitThe full 40-character SHA, lowercased. Read from the url when it carries all 40 characters; an abbreviated SHA in the url needs the full one as external_id.
claude_sessionThe session's UUID.
notion_pageThe page id.
figma_fileThe file key.
repo_pathThe path, which the app also shows as the link's title.
github_branchWhatever you pass, or nothing.

What the app shows

A GitHub link's title comes from its url (owner/repo #123), so metadata on GitHub kinds is kept and returned but not displayed. The app reads title, snippet and icon_emoji on a Notion page, file_name and thumbnail_url on a Figma file, and last_synced_at on any kind ("synced 3h ago"). status on a pull request (open, merged or closed) feeds the pull request counts on the Work list.

Reading and writing

This endpoint only reads: GET /api/v1/external-links filters by feature_id, kind, url, external_id and status, and POST, PATCH and DELETE are refused with a 403. Links are written by their own actions, which apply the rules above:

WhereAttachChangeRemove
MCPattach_artifact, or attach_external_link through execute_actionupdate_external_link through execute_actiondetach_external_link through execute_destructive_action
The appThe feature dialog's link pickerThe link's remove button

Over MCP, get_feature returns every link on a feature as external_links, and list_entities or count_entities on external-links finds them by feature (its sequence id or link works as well as the UUID), url or external_id. See MCP server.

Endpoints

GET/api/v1/external-links
GET/api/v1/external-links/{id}

Writable fields

This endpoint only reads. The fields below are what the type’s own actions take over MCP and the Copilot (see its description above).

FieldTypeConstraintsNotes
feature_id*uuidThe feature (Work item) the link is on.
kind*stringgithub_pr | github_issue | github_branch | github_commit | claude_session | notion_page | figma_file | repo_path | claude_artifact | other_link
url*stringThe link itself, http or https.
external_idstringThe provider's id for the link, one format per kind: owner/repo#number for github_pr and github_issue (ProductOS-Inc/ProductOS#123; derived from the url when left out, and a bare number is completed from it), the full 40-character SHA for github_commit (read from the url when it carries all of it), the session UUID for claude_session, the page id for notion_page, the file key for figma_file, the path for repo_path, the artifact id for claude_artifact (read from the url when left out). github_branch and other_link keep what you pass, usually nothing.
statusstringWhere the linked item stands. For github_pr the app counts open, merged and closed pull requests on the Work list; other kinds leave it empty.
metadatajsonExtra JSON kept on the link and returned by every read. The app shows title, snippet and icon_emoji on a notion_page, file_name and thumbnail_url on a figma_file, title on a claude_artifact or other_link (the name the row shows instead of its url), and last_synced_at on any kind as "synced 3h ago". A GitHub link's title comes from its url, so metadata on github kinds is stored but not displayed.

Listing & filtering

  • Pagination: ?limit=50&offset=0 · max limit 200
  • Default sort: ?sort=created_at&order=desc
  • Sortable fields: created_at · updated_at · kind
  • Search (?q=): url, external_id
  • Filters: ?feature_id= ?kind= ?url= ?external_id= ?status= , repeat any filter to OR multiple values

Examples

curlbash
curl 'https://productos.ca/api/v1/external-links?limit=10' \
  -H "Authorization: Bearer $TOKEN"

Try it out

Paste a token and hit a real endpoint. The request goes from your browser straight to /api/v1/external-links. The token is held in this page only for the request and is never stored.

Try it out

Issue tokens in Settings → API tokens. Token never leaves this page.

GET/api/v1/external-links?limit=3

Generated from the API's entity configuration. This page is rebuilt on every deploy, so it always matches the live API.