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 usecreate_portal/pipefy portal create.
Prerequisites
- Organization id: UUID or numeric org id from
pipefy org get/ the Pipefy URL (examples below use fictional123456789perfixture_ids.py). SDK resolves numeric ids before Interfaces calls. The org you pass tolist_portals/create_portalmust be the same org your token can write on. - Portal writes: token needs
create_portaland/ormanage_portalson 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_portalswith the intendedorganization_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_portalsand portal admin in Pipefy (not onlycanManagePortalson 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_portalon an existing main portal over delete +create_portalon orgs you reuse for testing. delete_portalon the main removes one interface UUID; orphan sub-portals can remain unless deleted first.- After deleting the main,
create_portalmay fail withMenu already created(org menu state persists whilemainPortalis null). Recovery: delete orphan sub-portals, use Pipefy admin/support, or bootstrap content on the surviving UUID — do not switch to rawcreateInterface, which leaves a skeleton main page ("Page", 0 elements) and a broken builder; idempotentcreate_portalwill 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 internal_api). create_portal_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
uuidwheresubTypeis the main portal.Inspect structure
MCP:
get_portal portal_uuid="<MAIN_PORTAL_UUID>"CLI:
pipefy portal get <MAIN_PORTAL_UUID>Note
pages[],elements[], andformselement ids. If the main page has zero elements, runcreate_portal_page(title only) on that portal before adding widgets.Optional — add a
formselement (if no templatedformsslot 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_elementreturns an opaque orINTERNAL_SERVER_ERRORfrom Interfaces,duplicate_portal_elementfrom an existing link on the sameportal_uuidandpage_idinstead 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_portalwill list it undersubPortals[]withpublished: false— the main hub UI is unchanged until step 5.Publish on a
formselementMCP:
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[].publishedistrue. 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 public
Steps — pages, layout, and safe edits
Use a disposable page for element/layout experiments on a shared org main portal:
create_portal_pagewith a unique title (e.g.Agent smoke 2026-06-01).- Run
create_portal_element,update_portal_element,duplicate_portal_element,update_portal_page_layouton that page only. delete_portal_pagewith 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=false
Then 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> --yes
Delete sub-portal interface (irreversible):
MCP two-step delete_sub_portal (echo confirmation_token) / CLI:
pipefy portal sub-portal delete <SUB_PORTAL_UUID> --yes
MCP response shape
- Read tools return
{ success: true, data: { ... } }whenPIPEFY_MCP_UNIFIED_ENVELOPEis enabled (default). Parsedataforportals,pages,subPortals, etc. - GraphQL/transport failures →
{ success: false, error: { message: "..." } }— do not treat transport errors as success. PERMISSION_DENIEDon portal tools usually namescreate_portalormanage_portals. Re-check org id, token, and SAjoinAsAdmin(see Confirm access).- Only
PERMISSION_DENIEDis rewritten to the portal permission hint; other GraphQL codes surface as generic errors with the API message. - Destructive deletes: default
confirm=falsereturns a preview (requires_confirmation: true,confirmation_token); call again withconfirm=trueand 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 butpublished: falseuntil publish. - After publish:
subPortals[].publishedistrueand the main page shows the wiredformsslot. - After unpublish:
publishedisfalsewithout 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 introspectionskills/introspection/pipefy-introspection/SKILL.md— verify Interfaces / internal_api mutations before changing tools