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:
. Parity matrix:
. Env vars:
.
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 (, , etc.).
- Attach, publish, unpublish, or delete sub-portals on the main portal.
Do not use for:
- Pipes, phases, cards, or automations — see , .
- Raw GraphQL when a portal tool exists — prefer the tools below.
- Bootstrapping a portal via undocumented GraphQL — always use / .
Prerequisites
- Organization id: UUID or numeric org id from / the Pipefy URL (examples below use fictional per ). SDK resolves numeric ids before Interfaces calls. The org you pass to / must be the same org your token can write on.
- Portal writes: token needs and/or on that org. Many service accounts only have pipe/card scope on their default org → on portal mutations even when reads succeed elsewhere.
- One main portal per org — is idempotent (second call returns the same portal UUID).
- Cursor MCP: after changing in , 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 with the intended .
- 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 and portal admin in Pipefy (not only on a read query from another org).
If
or
returns
but the user insists the org role is correct: Pipefy may require
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 (). Created with (findOrCreateInterfaceByTemplate
). |
| Usually one row — the main portal only (filter ). Sub-portals do not appear here. |
| Sub-portals | Separate entities (). Listed under → . |
| UI on the main hub | Creating sub-portals does not add tiles to the main page. You must publish (or attach) on a element via / update_sub_portal_element
. |
| End-user visibility | Sub-portals with exist in the API but are invisible to portal visitors until published. |
| Public main hub | Main portal is always on . Public access = update_portal(visibility="public")
, not the flag. |
Main portal lifecycle
- Prefer on an existing main portal over delete + on orgs you reuse for testing.
- on the main removes one interface UUID; orphan sub-portals can remain unless deleted first.
- After deleting the main, may fail with (org menu state persists while is null). Recovery: delete orphan sub-portals, use Pipefy admin/support, or bootstrap content on the surviving UUID — do not switch to raw , which leaves a skeleton main page (, 0 elements) and a broken builder; idempotent will keep returning that UUID.
Empty main page
If
shows a main page with
no elements, call
with
only (no
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
inside
— validation fails at create time.
Schema notes
| Topic | Rule |
|---|
| Response ids | GraphQL field is ; MCP/CLI expose (same value). |
| on list | does not return — call . |
| Sub-portal in layout | Tiles may appear under with even when top-level is empty. |
| Publish wire | Use / update_sub_portal_element
on an existing element ( on internal_api). with is not a substitute for publish. |
| Element metadata | is replace-all — send the full JSON every time. |
| Metadata keys | → (not ); → / (not / ). |
| Layout JSON | update_portal_page_layout
expects an array of row objects (, , children: [elementUuid, ...]
). Copy from . A wrapper like fails API validation. |
| Page grid vs elements | does not update the layout grid; appends layout rows; 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 |
|---|
| | Yes |
| | Yes |
| | No |
| | No |
| | No |
| pipefy portal page create
| No |
| pipefy portal page update
| No |
| pipefy portal page delete
| No |
| | No |
update_portal_page_layout
| pipefy portal page layout update
| No |
| pipefy portal element create
| No |
| pipefy portal element update
| No |
| pipefy portal element delete
| No |
| pipefy portal element duplicate
| No |
| pipefy portal sub-portal create
| No |
update_sub_portal_element
| pipefy portal sub-portal attach
| No |
| pipefy portal sub-portal publish
| No |
| pipefy portal sub-portal unpublish
| No |
delete_sub_portal_element
| pipefy portal sub-portal detach
| No |
| pipefy portal sub-portal delete
| No |
Element
values (15):
,
,
,
,
,
,
,
,
,
,
,
,
,
,
.
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:
bash
pipefy portal list --organization-uuid 123456789
pipefy portal create --organization-uuid 123456789
Capture
where
is the main portal.
-
Inspect structure
MCP:
get_portal portal_uuid="<MAIN_PORTAL_UUID>"
CLI:
bash
pipefy portal get <MAIN_PORTAL_UUID>
Note
,
, and
element ids. If the main page has
zero elements, run
(title only) on that portal before adding widgets.
-
Optional — add a element (if no templated
slot exists)
MCP:
create_portal_element page_id="<PAGE_ID>" type="forms" metadata={"name": "Request access", "gridMap": {"height": 66, "columns": 4, "minColumns": 4}}
If
returns an opaque or
from Interfaces,
from an existing link on the
same and
instead of retrying create blindly.
CLI:
bash
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:
bash
pipefy portal sub-portal create --main-portal-uuid <MAIN_PORTAL_UUID> --name "Partner hub"
Capture the sub-portal
.
will list it under with — the main hub UI is unchanged until step 5.
-
MCP:
publish_sub_portal portal_uuid="<MAIN_PORTAL_UUID>" element_id="<FORMS_ELEMENT_ID>" sub_portal_uuid="<SUB_PORTAL_UUID>"
CLI:
bash
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
is
. 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:
bash
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:
- with a unique title (e.g. ).
- Run , , ,
update_portal_page_layout
on that page only.
- with MCP preview then , or CLI .
: ,
, and
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
from
for that page and send the full array back with intentional edits. Never invent
stubs.
: pass a non-empty
list with no duplicates. If the raw response exposes nested
, treat the operation as failed even when the MCP envelope looks ambiguous.
Link element metadata (create/update, full replace):
json
{
"gridMap": { "height": 64, "columns": 4, "minColumns": 4 },
"linkUrl": "https://example.com",
"linkName": "Example link"
}
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:
bash
pipefy portal sub-portal unpublish <MAIN_PORTAL_UUID> <FORMS_ELEMENT_ID>
Detach element wiring (destructive — preview/confirm on MCP,
on CLI):
MCP:
delete_sub_portal_element portal_uuid="<MAIN_PORTAL_UUID>" element_id="<FORMS_ELEMENT_ID>" confirm=false
CLI:
bash
pipefy portal sub-portal detach <MAIN_PORTAL_UUID> <FORMS_ELEMENT_ID> --yes
Delete sub-portal interface (irreversible):
bash
pipefy portal sub-portal delete <SUB_PORTAL_UUID> --yes
MCP response shape
- Read tools return
{ success: true, data: { ... } }
when PIPEFY_MCP_UNIFIED_ENVELOPE
is enabled (default). Parse for , , , etc.
- GraphQL/transport failures →
{ success: false, error: { message: "..." } }
— do not treat transport errors as success.
- on portal tools usually names or . Re-check org id, token, and SA (see Confirm access).
- Only is rewritten to the portal permission hint; other GraphQL codes surface as generic errors with the API message.
- Destructive deletes: default returns a preview (
requires_confirmation: true
); call again with only after explicit human approval.
CLI
prints the raw SDK payload (no
wrapper).
Success criteria
- returns the org main portal (typically one row); returns the same UUID on repeat.
- shows expected / after writes.
- After , sub-portal exists in API but until publish.
- After publish: is and the main page shows the wired slot.
- After unpublish: is without deleting the sub-portal entity (unless you called ).
- 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 |
|---|
| on writes | Wrong org, missing , or SA not joined on interface | Same org as ; user runs portal admin join; try human admin token |
| Reads OK, writes fail on org | Token is human on org A, numeric id is org B | Align with token membership |
| on | Main deleted but org menu state remains | Delete orphan sub-portals; avoid raw ; bootstrap with on existing UUID |
| Main page empty in builder | Portal created outside template path | (title only) for templated elements |
| missing on list | Expected | |
| Many subs in , empty main UI | Sub-portals not published to slots | per sub + |
| Publish no effect | Wrong element type or skipped internal_api wire | → element → |
| empty but UI shows tile | Linked under | Inspect in |
| opaque / 500 | Interfaces instability on some orgs | from existing widget on same page |
| Portal viewer HTTP 500 | Orphan children or wrong layout shape | Copy/fix layout from ; delete disposable smoke page |
| Nested | API rejected mutation | Read ; do not assume top-level success |
| Validation on element | Wrong metadata keys or partial update | Full metadata blob; / per type |
See also
- — endpoints, wire naming, maintainer introspection
skills/introspection/pipefy-introspection/SKILL.md
— verify Interfaces / internal_api mutations before changing tools