원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.
Portal setup
Configure an organization's Pipefy portal: bootstrap the main hub, add pages and widgets, wire and publish sub-portals. 20 MCP tools (Interfaces GraphQL + internal_api for sub-portal wiring).
Deep reference: `docs/mcp/tools/portal.md`. Parity matrix: `docs/parity.md`. Env vars: `docs/config.md`.
When to use
- "Create our company portal", "list portals for org X", "publish a sub-portal".
- Add or change portal pages, layout, or page elements (
forms,link, etc.). - Attach, publish, unpublish, or delete sub-portals on the main portal.
Do not use for:
- Pipes, phases, cards, or automations — see
skills/pipes-and-cards/,skills/automations/. - Raw GraphQL when a portal tool exists — prefer the tools below.
- Bootstrapping a portal via undocumented
createInterfaceGraphQL — always use `create_portal` / `pipefy portal create`.
Prerequisites
- Organization id: UUID or numeric org id from
pipefy org get/ the Pipefy URL (examples below use fictional123456789per `fixture_ids.py`). SDK resolves numeric ids before Interfaces calls. The org you pass to `list_portals` / `create_portal` must be the same org your token can write on. - Portal writes: token needs `create_portal` and/or `manage_portals` on that org. Many service accounts only have pipe/card scope on their default org →
PERMISSION_DENIEDon portal mutations even when reads succeed elsewhere. - One main portal per org —
create_portalis idempotent (second call returns the same portal UUID). - Cursor MCP: after changing
PIPEFY_*in.env, restart the MCP server so tools pick up the new credentials.
Confirm access before writes
Reads on the wrong org can succeed while Interfaces writes fail. Before page/element/sub-portal mutations:
- Call `list_portals` with the intended
organization_uuid. - Ensure the token is meant for that org (service account email vs human user on a different org is a common mismatch).
- Prefer an org where the account has `manage_portals` and portal admin in Pipefy (not only
canManagePortalson a read query from another org).
If `update_portal` or `delete_portal` returns PERMISSION_DENIED but the user insists the org role is correct: Pipefy may require `joinAsAdmin` on that portal interface for service accounts (Interfaces mutation, not shipped as MCP/CLI). The user must join as portal admin once in the UI (or via GraphQL) per portal UUID before SA writes succeed.
How portals are organized
| Concept | What to expect |
|---|---|
| Main portal | At most one per org (subType: portal). Created with `create_portal` (findOrCreateInterfaceByTemplate). |
| `list_portals` | Usually one row — the main portal only (filter portal). Sub-portals do not appear here. |
| Sub-portals | Separate entities (create_sub_portal). Listed under `get_portal` → `subPortals[]`. |
| UI on the main hub | Creating sub-portals does not add tiles to the main page. You must publish (or attach) on a `forms` element via `publish_sub_portal` / `update_sub_portal_element`. |
| End-user visibility | Sub-portals with `published: false` exist in the API but are invisible to portal visitors until published. |
| Public main hub | Main portal `published` is always true on get_portal. Public access = `update_portal(visibility="public")`, not the published flag. |
Main portal lifecycle
- Prefer `update_portal` on an existing main portal over delete + `create_portal` on orgs you reuse for testing.
- `delete_portal` on the main removes one interface UUID; orphan sub-portals can remain unless deleted first.
- After deleting the main, `create_portal` may fail with `Menu already created` (org menu state persists while
mainPortalis null). Recovery: delete orphan sub-portals, use Pipefy admin/support, or bootstrap content on the surviving UUID — do not switch to raw `createInterface`, which leaves a skeleton main page ("Page", 0 elements) and a broken builder; idempotent `create_portal` will keep returning that UUID.
Empty main page
If `get_portal` shows a main page with no elements, call `create_portal_page` with `title` only (no elements in the request). The API typically returns a page with ~14 templated widgets (text, forms, links, etc.). Use that page for publish slots and element tests. Do not pass type: subPortal inside `create_portal_page` — validation fails at create time.
Schema notes
| Topic | Rule |
|---|---|
| Response ids | GraphQL field is id; MCP/CLI expose `uuid` (same value). |
| `published` on list | `list_portals` does not return `published` — call `get_portal`. |
| Sub-portal in layout | Tiles may appear under `pages[].elements[]` with type: subPortal even when top-level subPortals[] is empty. |
| Publish wire | Use `publish_sub_portal` / `update_sub_portal_element` on an existing `forms` element (updateSubPortalElement on internalapi). **`createportal_element with type: subPortal`** is not a substitute for publish. |
| Element metadata | `update_portal_element` is replace-all — send the full metadata JSON every time. |
| Metadata keys | forms → name (not formId); link → linkName / linkUrl (not url / label). |
| Layout JSON | `update_portal_page_layout` expects an array of row objects (id, type: "row", children: [elementUuid, ...]). Copy from `get_portal`. A wrapper like { "rows": [...] } fails API validation. |
| Page grid vs elements | `create_portal_element` does not update the layout grid; `duplicate_portal_element` appends layout rows; `delete_portal_element` does not remove layout refs unless you update layout — orphan refs can break the portal viewer (HTTP 500). |
Tools needed
| Tool (MCP) | CLI equivalent | Read-only |
|---|---|---|
list_portals | pipefy portal list | Yes |
get_portal | pipefy portal get | Yes |
create_portal | pipefy portal create | No |
update_portal | pipefy portal update | No |
delete_portal | pipefy portal delete | No |
create_portal_page | pipefy portal page create | No |
update_portal_page | pipefy portal page update | No |
delete_portal_page | pipefy portal page delete | No |
sort_portal_pages | pipefy portal page sort | No |
update_portal_page_layout | pipefy portal page layout update | No |
create_portal_element | pipefy portal element create | No |
update_portal_element | pipefy portal element update | No |
delete_portal_element | pipefy portal element delete | No |
duplicate_portal_element | pipefy portal element duplicate | No |
create_sub_portal | pipefy portal sub-portal create | No |
update_sub_portal_element | pipefy portal sub-portal attach | No |
publish_sub_portal | pipefy portal sub-portal publish | No |
unpublish_sub_portal | pipefy portal sub-portal unpublish | No |
delete_sub_portal_element | pipefy portal sub-portal detach | No |
delete_sub_portal | pipefy portal sub-portal delete | No |
Element type values (15): text, table, field, embedLink, embedVideo, embedImage, button, divider, link, forms, pages, subPortal, automationButton, contentBlock, document.
Steps — happy path (main portal + sub-portal publish)
- List or bootstrap the main portal
MCP:
list_portals organization_uuid="123456789"Expect at most one main portal row. If none:
MCP:
create_portal organization_uuid="123456789"CLI:
pipefy portal list --organization-uuid 123456789
pipefy portal create --organization-uuid 123456789Capture uuid where subType is the main portal.
- Inspect structure
MCP:
get_portal portal_uuid="<MAIN_PORTAL_UUID>"CLI:
pipefy portal get <MAIN_PORTAL_UUID>Note pages[], elements[], and `forms` element ids. If the main page has zero elements, run `create_portal_page` (title only) on that portal before adding widgets.
- Optional — add a `forms` element (if no templated
formsslot exists)
MCP:
create_portal_element page_id="<PAGE_ID>" type="forms" metadata={"name": "Request access", "gridMap": {"height": 66, "columns": 4, "minColumns": 4}}If `create_portal_element` returns an opaque or INTERNAL_SERVER_ERROR from Interfaces, `duplicate_portal_element` from an existing link on the same portal_uuid and page_id instead of retrying create blindly.
CLI:
pipefy portal element create --page-id <PAGE_ID> --type forms \
--metadata '{"name":"Request access","gridMap":{"height":66,"columns":4,"minColumns":4}}'- Create a sub-portal
MCP:
create_sub_portal main_portal_uuid="<MAIN_PORTAL_UUID>" name="Partner hub"CLI:
pipefy portal sub-portal create --main-portal-uuid <MAIN_PORTAL_UUID> --name "Partner hub"Capture the sub-portal uuid. `get_portal` will list it under `subPortals[]` with `published: false` — the main hub UI is unchanged until step 5.
- Publish on a `forms` element
MCP:
publish_sub_portal portal_uuid="<MAIN_PORTAL_UUID>" element_id="<FORMS_ELEMENT_ID>" sub_portal_uuid="<SUB_PORTAL_UUID>"CLI:
pipefy portal sub-portal publish <MAIN_PORTAL_UUID> <FORMS_ELEMENT_ID> <SUB_PORTAL_UUID>- Verify publish state
MCP:
get_portal portal_uuid="<MAIN_PORTAL_UUID>"Success: target `subPortals[].published` is `true`. End users can see the sub-portal only after this (and hub visibility rules).
- Optional — make the main hub public
MCP:
update_portal portal_uuid="<MAIN_PORTAL_UUID>" visibility="public"CLI:
pipefy portal update <MAIN_PORTAL_UUID> --visibility publicSteps — pages, layout, and safe edits
Use a disposable page for element/layout experiments on a shared org main portal:
- `create_portal_page` with a unique title (e.g.
Agent smoke 2026-06-01). - Run `create_portal_element`, `update_portal_element`, `duplicate_portal_element`, `update_portal_page_layout` on that page only.
- `delete_portal_page` with MCP two-step (
confirmation_tokenfrom the preview, thenconfirm=true), or CLI `--yes`.
`duplicate_portal_element`: element_id, portal_uuid, and page_id must refer to the same page that already contains the source element (duplicate on the same page, not cross-page).
`update_portal_page_layout`: read layout from `get_portal` for that page and send the full array back with intentional edits. Never invent { "rows": [ ... ] } stubs.
`sort_portal_pages`: pass a non-empty page_ids list with no duplicates. If the raw response exposes nested success: false, treat the operation as failed even when the MCP envelope looks ambiguous.
Link element metadata (create/update, full replace):
{
"gridMap": { "height": 64, "columns": 4, "minColumns": 4 },
"linkUrl": "https://example.com",
"linkName": "Example link"
}Two-step destructive deletes
MCP deletes (delete_portal, delete_portal_page, delete_portal_element, delete_sub_portal, delete_sub_portal_element) return a preview with confirmation_token. Echo that token with confirm=true on the second call. CLI uses --yes. unpublish_sub_portal is not gated.
Steps — unpublish or remove sub-portal
Unpublish (keeps sub-portal entity; visitors lose access):
MCP:
unpublish_sub_portal portal_uuid="<MAIN_PORTAL_UUID>" element_id="<FORMS_ELEMENT_ID>"CLI:
pipefy portal sub-portal unpublish <MAIN_PORTAL_UUID> <FORMS_ELEMENT_ID>Detach element wiring (destructive: MCP two-step with confirmation_token, --yes on CLI):
MCP:
delete_sub_portal_element portal_uuid="<MAIN_PORTAL_UUID>" element_id="<FORMS_ELEMENT_ID>" confirm=falseThen after approval, echo the preview's confirmation_token:
delete_sub_portal_element portal_uuid="<MAIN_PORTAL_UUID>" element_id="<FORMS_ELEMENT_ID>" confirm=true confirmation_token="<token from preview>"CLI:
pipefy portal sub-portal detach <MAIN_PORTAL_UUID> <FORMS_ELEMENT_ID> --yesDelete sub-portal interface (irreversible):
MCP two-step delete_sub_portal (echo confirmation_token) / CLI:
pipefy portal sub-portal delete <SUB_PORTAL_UUID> --yesMCP response shape
- Read tools return
{ success: true, data: { ... } }whenPIPEFY_MCP_UNIFIED_ENVELOPEis enabled (default). Parse `data` forportals,pages,subPortals, etc. - GraphQL/transport failures →
{ success: false, error: { message: "..." } }— do not treat transport errors as success. - `PERMISSION_DENIED` on portal tools usually names `create_portal` or `manage_portals`. Re-check org id, token, and SA `joinAsAdmin` (see Confirm access).
- Only `PERMISSION_DENIED` is rewritten to the portal permission hint; other GraphQL codes surface as generic errors with the API message.
- Destructive deletes: default `confirm=false` returns a preview (
requires_confirmation: true,confirmation_token); call again with `confirm=true` and that token only after explicit human approval.
CLI --json prints the raw SDK payload (no success wrapper).
Success criteria
list_portalsreturns the org main portal (typically one row);create_portalreturns the same UUID on repeat.get_portalshows expectedpages/elementsafter writes.- After `create_sub_portal`, sub-portal exists in API but `published: false` until publish.
- After publish: `subPortals[].published` is
trueand the main page shows the wired `forms` slot. - After unpublish: `published` is
falsewithout deleting the sub-portal entity (unless you calleddelete_sub_portal). - After layout/element edits on a disposable page, main portal pages used in production still open in the builder (no HTTP 500).
Failure modes
| Symptom | Likely cause | Recovery |
|---|---|---|
PERMISSION_DENIED on writes | Wrong org, missing manage_portals, or SA not joined on interface | Same org as list_portals; user runs portal admin join; try human admin token |
Reads OK, writes fail on admin org | Token is human on org A, numeric id is org B | Align organization_uuid with token membership |
Menu already created on create_portal | Main deleted but org menu state remains | Delete orphan sub-portals; avoid raw createInterface; bootstrap with create_portal_page on existing UUID |
| Main page empty in builder | Portal created outside create_portal template path | create_portal_page (title only) for templated elements |
published missing on list | Expected | get_portal |
Many subs in get_portal, empty main UI | Sub-portals not published to forms slots | publish_sub_portal per sub + forms element_id |
| Publish no effect | Wrong element type or skipped internal_api wire | get_portal → forms element → publish_sub_portal |
subPortals[] empty but UI shows tile | Linked under pages[].elements | Inspect type: subPortal in elements[] |
create_portal_element opaque / 500 | Interfaces instability on some orgs | duplicate_portal_element from existing widget on same page |
| Portal viewer HTTP 500 | Orphan layout children or wrong layout shape | Copy/fix layout from get_portal; delete disposable smoke page |
Nested success: false | API rejected mutation | Read error.message; do not assume top-level success |
| Validation on element | Wrong metadata keys or partial update | Full metadata blob; linkName / name per type |
See also
- `docs/mcp/tools/portal.md` — endpoints, wire naming, maintainer introspection
- `skills/introspection/pipefy-introspection/SKILL.md` — verify Interfaces / internal_api mutations before changing tools

