Port workflows
A workflow is Port's visual, node-based automation layer. Nodes (triggers,
actions, conditions, inputs) connect into a graph; running the workflow walks
that graph, passing each node's output to the next as JQ-templated data.
Use this skill when
Use this skill to author or edit workflow JSON: adding a self-service or
event trigger, adding an action node (webhook, upsert entity, an integration
action, Kafka, Cursor Agent, or AI), branching with a condition node, gating
on human approval with an input node, or wiring the connections and JQ
templates between them. This skill is reference-only: the JSON it produces is
valid without a live Port account, though applying it needs API credentials
(see Prerequisites).
Out of scope: Port's older, single-step Actions & Automations model (not
covered by a skill in this repo yet; see the
comparison table
for when to use which), blueprint schema design (port-blueprints), and
CI/CD pipeline files such as .github/workflows/*.yml, GitHub Actions is a
separate product that a workflow can trigger (via INTEGRATION_ACTION)
but does not replace.
Prerequisites
- Go over the
port-getting-started skill first if this is your first time
working with Port.
- To create or update workflows without MCP: a Port
CLIENT_ID and
CLIENT_SECRET (... menu > Credentials in the Port app),
exchanged for a bearer token at POST https://api.port.io/v1/auth/access_token.
- If Port's MCP server is connected, this skill can use its workflow tools
(
list_workflows, get_workflow, upsert_workflow) to read or apply a
workflow directly instead of you copying JSON in by hand, but the raw API
calls in Step 6 always
work as a fallback. Search search_port_knowledge_sources for anything
this skill doesn't cover.
- Workflows are in open beta: cross-check unfamiliar fields against
docs.port.io/workflows before
shipping.
Step 1 - Sketch the graph and pick trigger type(s)
Precondition: you know what should start the automation and what it should do.
Action: decide SELF_SERVE_TRIGGER (a user submits a form on demand),
EVENT_TRIGGER (an entity is created, updated, deleted, or a timer property
expires), or both, a workflow can define multiple trigger nodes feeding the
same downstream graph. Then list the actions in order: webhook calls, entity
upserts, integration dispatches, AI steps, with any branching (condition) or
human approval (input) points.
Fallback: if the trigger is unclear, default to a self-service trigger, it is
the safer, opt-in default and easiest to test.
Step 2 - Configure the trigger node(s)
Precondition: trigger type chosen.
Action: for SELF_SERVE_TRIGGER, define userInputs.properties (and
required); for EVENT_TRIGGER, define event.type and
blueprintIdentifier, plus an optional JQ condition to filter which events
fire the workflow. Full field reference, including contexts (bolt-menu and
create-entity surfacing) and variant, is in
references/node-types.md.
Fallback: never add title, icon, or description to a trigger node,
those fields are ignored there and belong on action nodes instead.
Step 3 - Add action and flow nodes
Precondition: trigger is defined.
Action: add one node per step. See
references/node-types.md for the full field
reference and JSON shape of every node type: WEBHOOK, UPSERT_ENTITY,
INTEGRATION_ACTION, KAFKA, CURSOR_AGENT, AI_AGENT, AI, CONDITION,
INPUT. Reference upstream data with JQ templates, see
references/data-flow.md.
Fallback: if a step just needs to call an HTTP API, prefer WEBHOOK over
building a custom integration, and prefer UPSERT_ENTITY over a raw
WEBHOOK call to Port's entity API, it is simpler and less error-prone.
Step 4 - Wire connections
Precondition: nodes exist.
Action: add a connections entry (sourceIdentifier, targetIdentifier)
per edge. CONDITION and INPUT nodes need sourceOutletIdentifier (or
fallback: true) on every outgoing connection, matching one of the node's
outlets. Trigger nodes can only be sources, never targets. Each node (and
each outlet) can have at most one outgoing connection, fan-out is not
supported, chain nodes sequentially instead.
Fallback: got a fan-out requirement (one node feeding two independent next
steps)? Split it into two workflows, or serialize the two branches behind a
single downstream node instead of trying to fan out directly.
Step 5 - Set self-service permissions
Precondition: workflow has a SELF_SERVE_TRIGGER node.
Action: set permissions on the trigger node's config. Omitted or {}
means Admin only. See
references/permissions.md for static
(roles/users/teams) and dynamic policy rules (based on user, team, or
form-input properties).
Fallback: event triggers have no permissions field, event-driven workflows
run under the organization's automation identity, not a specific user.
Step 6 - Create or update via the Port API
Precondition: you have a bearer token and a complete workflow JSON body
(identifier, nodes, connections, at minimum).
Action: use the endpoints below (workflow_identifier is the workflow's
identifier).
| Operation |
Method |
Path |
| Create |
POST |
/v1/workflows |
| List |
GET |
/v1/workflows |
| Get |
GET |
/v1/workflows/{workflow_identifier} |
| Update (creates a new version) |
PUT |
/v1/workflows/{workflow_identifier} |
| Delete |
DELETE |
/v1/workflows/{workflow_identifier} |
| Trigger a run |
POST |
/v1/workflows/{workflow_identifier}/runs |
curl -L -X POST 'https://api.port.io/v1/workflows' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
--data @workflow.json
Fallback: a 409 on create means the identifier is taken, PUT an update
instead. A 422 usually means a connections entry references a node
identifier that doesn't exist, or a CONDITION/INPUT connection is missing
sourceOutletIdentifier, check Common pitfalls below.
Complete examples
assets/self-service-deploy-workflow.json -
self-service trigger, environment branching with a CONDITION node, and a
Slack notification.
assets/event-driven-ai-incident-response.json -
event trigger with a JQ filter, an AI node with outputSchema for
structured output, and an entity upsert.
assets/approval-workflow.json - self-service
trigger gated by an INPUT approval step before a production deploy.
Common pitfalls
| Symptom |
Cause |
Fix |
.outputs.my-node.field silently returns nothing |
Hyphens in a node identifier break JQ dot notation (- reads as subtraction) |
Use bracket notation .outputs["my-node"].field, or keep node identifiers snake_case so dot notation works |
Adding variables makes existing output references break |
variables replaces a node's entire default output, response and friends disappear |
Re-declare anything downstream still needs inside variables, e.g. "response": "{{ .result.response }}" |
CONDITION/INPUT connection has no effect, or workflow rejects it |
Missing sourceOutletIdentifier (or fallback: true) on the connection |
Every outgoing connection from a CONDITION or INPUT node must set sourceOutletIdentifier matching one of the node's outlets, or fallback: true |
UPSERT_ENTITY (or another structured node) after an AI node gets nothing |
The AI node emitted free-form text, not JSON |
Set outputSchema (JSON Schema, type: "object") on the AI node, then parse with {{ .outputs.node_id.response | fromjson | .field }} |
Event trigger fires but .outputs.trigger.diff.before is unexpectedly null |
ENTITY_CREATED has no before, ENTITY_DELETED has no after |
Branch on .outputs.trigger.action (CREATE/UPDATE/DELETE) before reading diff.before/diff.after |
| Workflow silently does nothing when a node fails mid-graph |
Default onFailure is "terminate", the whole run stops |
Set "onFailure": "continue" on WEBHOOK, UPSERT_ENTITY, KAFKA, or INTEGRATION_ACTION nodes that shouldn't block the rest of the run |
| GitHub Actions workflow never fires |
Used WEBHOOK to call the GitHub API by hand, or used the wrong field name for status reporting |
Use INTEGRATION_ACTION with integrationProvider: "github-ocean", and reportWorkflowStatus (not reportStatus) to get status back in Port |
Quick reference
- Node types:
SELF_SERVE_TRIGGER, EVENT_TRIGGER (triggers); WEBHOOK,
UPSERT_ENTITY, INTEGRATION_ACTION, KAFKA, CURSOR_AGENT, AI_AGENT,
AI (actions); CONDITION, INPUT (flow control). Full reference:
references/node-types.md.
- Data access:
{{ .outputs["node_id"].field }} (previous node output),
{{ .outputs.trigger.field }} (always aliases the trigger that fired),
{{ .secrets["name"] }}, {{ .workflowRun.trigger.by.email }}. Full
reference: references/data-flow.md.
- Permissions:
{}/omitted = Admin only; roles/users/teams = static OR
match; policy = dynamic rules over user, userTeams, or form
context. Full reference: references/permissions.md.
- Auth:
POST /v1/auth/access_token with clientId/clientSecret for a
bearer token; no Authorization header needed when a workflow node itself
calls https://api.port.io.
- Workflow top-level fields:
identifier (required, ≤60 chars,
^[A-Za-z0-9@_:-]+$), title, icon, description, category (groups
it in the UI), allowAnyoneToViewRuns (default true), nodes,
connections.
1---2name: port-workflows3description: Build Port workflows: node-based automations made of triggers (self-service forms, context lake events), action nodes (webhook, upsert entity, GitHub/GitLab/Azure DevOps integration actions, Kafka, Cursor Agent, AI), condition and input nodes, JQ templating between nodes, and self-service trigger permissions, authored as workflow JSON against the Port API. Use when asked to create a Port workflow, add a trigger to a workflow, add an action node, wire up a self-service workflow, add branching with a condition node, add an approval step to a workflow, build an event-driven automation in Port, or write workflow JSON. Port workflows are Port's own nodes-and-edges automation graph, not a CI/CD pipeline file like GitHub Actions.4license: MIT5---67# Port workflows89A workflow is Port's visual, node-based automation layer. Nodes (triggers,10actions, conditions, inputs) connect into a graph; running the workflow walks11that graph, passing each node's output to the next as JQ-templated data.1213## Use this skill when1415Use this skill to author or edit workflow JSON: adding a self-service or16event trigger, adding an action node (webhook, upsert entity, an integration17action, Kafka, Cursor Agent, or AI), branching with a condition node, gating18on human approval with an input node, or wiring the connections and JQ19templates between them. This skill is reference-only: the JSON it produces is20valid without a live Port account, though applying it needs API credentials21(see [Prerequisites](#prerequisites)).2223Out of scope: Port's older, single-step Actions & Automations model (not24covered by a skill in this repo yet; see the25[comparison table](https://docs.port.io/workflows/overview#comparison-with-actions--automations)26for when to use which), blueprint schema design (`port-blueprints`), and27CI/CD pipeline files such as `.github/workflows/*.yml`, GitHub Actions is a28separate product that a workflow can *trigger* (via `INTEGRATION_ACTION`)29but does not replace.3031## Prerequisites3233- Go over the `port-getting-started` skill first if this is your first time34 working with Port.35- To create or update workflows without MCP: a Port `CLIENT_ID` and36 `CLIENT_SECRET` (**...** menu > **Credentials** in the Port app),37 exchanged for a bearer token at `POST https://api.port.io/v1/auth/access_token`.38- If Port's MCP server is connected, this skill can use its workflow tools39 (`list_workflows`, `get_workflow`, `upsert_workflow`) to read or apply a40 workflow directly instead of you copying JSON in by hand, but the raw API41 calls in [Step 6](#step-6---create-or-update-via-the-port-api) always42 work as a fallback. Search `search_port_knowledge_sources` for anything43 this skill doesn't cover.44- Workflows are in open beta: cross-check unfamiliar fields against45 [docs.port.io/workflows](https://docs.port.io/workflows/overview) before46 shipping.4748## Step 1 - Sketch the graph and pick trigger type(s)4950Precondition: you know what should start the automation and what it should do.51Action: decide `SELF_SERVE_TRIGGER` (a user submits a form on demand),52`EVENT_TRIGGER` (an entity is created, updated, deleted, or a timer property53expires), or both, a workflow can define multiple trigger nodes feeding the54same downstream graph. Then list the actions in order: webhook calls, entity55upserts, integration dispatches, AI steps, with any branching (condition) or56human approval (input) points.57Fallback: if the trigger is unclear, default to a self-service trigger, it is58the safer, opt-in default and easiest to test.5960## Step 2 - Configure the trigger node(s)6162Precondition: trigger type chosen.63Action: for `SELF_SERVE_TRIGGER`, define `userInputs.properties` (and64`required`); for `EVENT_TRIGGER`, define `event.type` and65`blueprintIdentifier`, plus an optional JQ `condition` to filter which events66fire the workflow. Full field reference, including `contexts` (bolt-menu and67create-entity surfacing) and `variant`, is in68[references/node-types.md](references/node-types.md#trigger-nodes).69Fallback: never add `title`, `icon`, or `description` to a trigger node,70those fields are ignored there and belong on action nodes instead.7172## Step 3 - Add action and flow nodes7374Precondition: trigger is defined.75Action: add one node per step. See76[references/node-types.md](references/node-types.md) for the full field77reference and JSON shape of every node type: `WEBHOOK`, `UPSERT_ENTITY`,78`INTEGRATION_ACTION`, `KAFKA`, `CURSOR_AGENT`, `AI_AGENT`, `AI`, `CONDITION`,79`INPUT`. Reference upstream data with JQ templates, see80[references/data-flow.md](references/data-flow.md).81Fallback: if a step just needs to call an HTTP API, prefer `WEBHOOK` over82building a custom integration, and prefer `UPSERT_ENTITY` over a raw83`WEBHOOK` call to Port's entity API, it is simpler and less error-prone.8485## Step 4 - Wire connections8687Precondition: nodes exist.88Action: add a `connections` entry (`sourceIdentifier`, `targetIdentifier`)89per edge. `CONDITION` and `INPUT` nodes need `sourceOutletIdentifier` (or90`fallback: true`) on every outgoing connection, matching one of the node's91`outlets`. Trigger nodes can only be sources, never targets. Each node (and92each outlet) can have at most one outgoing connection, fan-out is not93supported, chain nodes sequentially instead.94Fallback: got a fan-out requirement (one node feeding two independent next95steps)? Split it into two workflows, or serialize the two branches behind a96single downstream node instead of trying to fan out directly.9798## Step 5 - Set self-service permissions99100Precondition: workflow has a `SELF_SERVE_TRIGGER` node.101Action: set `permissions` on the trigger node's `config`. Omitted or `{}`102means Admin only. See103[references/permissions.md](references/permissions.md) for static104(`roles`/`users`/`teams`) and dynamic `policy` rules (based on user, team, or105form-input properties).106Fallback: event triggers have no `permissions` field, event-driven workflows107run under the organization's automation identity, not a specific user.108109## Step 6 - Create or update via the Port API110111Precondition: you have a bearer token and a complete workflow JSON body112(`identifier`, `nodes`, `connections`, at minimum).113114Action: use the endpoints below (`workflow_identifier` is the workflow's115`identifier`).116117| Operation | Method | Path |118|---|---|---|119| Create | `POST` | `/v1/workflows` |120| List | `GET` | `/v1/workflows` |121| Get | `GET` | `/v1/workflows/{workflow_identifier}` |122| Update (creates a new version) | `PUT` | `/v1/workflows/{workflow_identifier}` |123| Delete | `DELETE` | `/v1/workflows/{workflow_identifier}` |124| Trigger a run | `POST` | `/v1/workflows/{workflow_identifier}/runs` |125126```bash127curl -L -X POST 'https://api.port.io/v1/workflows' \128 -H "Authorization: Bearer ${ACCESS_TOKEN}" \129 -H 'Content-Type: application/json' \130 --data @workflow.json131 ```132133Fallback: a `409` on create means the `identifier` is taken, `PUT` an update134instead. A `422` usually means a `connections` entry references a node135identifier that doesn't exist, or a `CONDITION`/`INPUT` connection is missing136`sourceOutletIdentifier`, check [Common pitfalls](#common-pitfalls) below.137138## Complete examples139140[assets/self-service-deploy-workflow.json](assets/self-service-deploy-workflow.json) -141self-service trigger, environment branching with a `CONDITION` node, and a142Slack notification.143144[assets/event-driven-ai-incident-response.json](assets/event-driven-ai-incident-response.json) -145event trigger with a JQ filter, an `AI` node with `outputSchema` for146structured output, and an entity upsert.147148[assets/approval-workflow.json](assets/approval-workflow.json) - self-service149trigger gated by an `INPUT` approval step before a production deploy.150151## Common pitfalls152153| Symptom | Cause | Fix |154|---|---|---|155| `.outputs.my-node.field` silently returns nothing | Hyphens in a node identifier break JQ dot notation (`-` reads as subtraction) | Use bracket notation `.outputs["my-node"].field`, or keep node identifiers snake_case so dot notation works |156| Adding `variables` makes existing output references break | `variables` **replaces** a node's entire default output, `response` and friends disappear | Re-declare anything downstream still needs inside `variables`, e.g. `"response": "{{ .result.response }}"` |157| `CONDITION`/`INPUT` connection has no effect, or workflow rejects it | Missing `sourceOutletIdentifier` (or `fallback: true`) on the connection | Every outgoing connection from a `CONDITION` or `INPUT` node must set `sourceOutletIdentifier` matching one of the node's `outlets`, or `fallback: true` |158| `UPSERT_ENTITY` (or another structured node) after an `AI` node gets nothing | The AI node emitted free-form text, not JSON | Set `outputSchema` (JSON Schema, `type: "object"`) on the AI node, then parse with `{{ .outputs.node_id.response \| fromjson \| .field }}` |159| Event trigger fires but `.outputs.trigger.diff.before` is unexpectedly `null` | `ENTITY_CREATED` has no `before`, `ENTITY_DELETED` has no `after` | Branch on `.outputs.trigger.action` (`CREATE`/`UPDATE`/`DELETE`) before reading `diff.before`/`diff.after` |160| Workflow silently does nothing when a node fails mid-graph | Default `onFailure` is `"terminate"`, the whole run stops | Set `"onFailure": "continue"` on `WEBHOOK`, `UPSERT_ENTITY`, `KAFKA`, or `INTEGRATION_ACTION` nodes that shouldn't block the rest of the run |161| GitHub Actions workflow never fires | Used `WEBHOOK` to call the GitHub API by hand, or used the wrong field name for status reporting | Use `INTEGRATION_ACTION` with `integrationProvider: "github-ocean"`, and `reportWorkflowStatus` (not `reportStatus`) to get status back in Port |162163## Quick reference164165- Node types: `SELF_SERVE_TRIGGER`, `EVENT_TRIGGER` (triggers); `WEBHOOK`,166 `UPSERT_ENTITY`, `INTEGRATION_ACTION`, `KAFKA`, `CURSOR_AGENT`, `AI_AGENT`,167 `AI` (actions); `CONDITION`, `INPUT` (flow control). Full reference:168 [references/node-types.md](references/node-types.md).169- Data access: `{{ .outputs["node_id"].field }}` (previous node output),170 `{{ .outputs.trigger.field }}` (always aliases the trigger that fired),171 `{{ .secrets["name"] }}`, `{{ .workflowRun.trigger.by.email }}`. Full172 reference: [references/data-flow.md](references/data-flow.md).173- Permissions: `{}`/omitted = Admin only; `roles`/`users`/`teams` = static OR174 match; `policy` = dynamic rules over `user`, `userTeams`, or `form`175 context. Full reference: [references/permissions.md](references/permissions.md).176- Auth: `POST /v1/auth/access_token` with `clientId`/`clientSecret` for a177 bearer token; no `Authorization` header needed when a workflow node itself178 calls `https://api.port.io`.179- Workflow top-level fields: `identifier` (required, ≤60 chars,180 `^[A-Za-z0-9@_:-]+$`), `title`, `icon`, `description`, `category` (groups181 it in the UI), `allowAnyoneToViewRuns` (default `true`), `nodes`,182 `connections`.