> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself ➜ 

Send Slack messages to users using Port Workflows

Implement with AI

Send this guide to your coding agent.

Prerequisite: Install Port MCP

Open plan mode if your tool supports it; otherwise present the plan below filled in and wait for my approval. Implement this Port guide in my org via MCP:

https://docs.port.io/guides/all/send-slack-message-to-user

Read the raw markdown version at https://docs.port.io/guides/all/send-slack-message-to-user.md - it contains every tab and code block without page markup.

Goal: get the guide's core flow working end-to-end in my org; adapting it to fit my existing setup takes priority over matching the guide 1:1.

Plan:
1. Confirm MCP is connected, in the right org, with sufficient permissions.
2. If the guide offers alternative implementation paths (tabs), pick the one matching my installed integrations and tools, confirm it with me, and implement only that path.
3. Diff the guide's data model (blueprints, properties, relations, workflows, actions, agents, automations, integrations, webhook data sources, secrets) against mine.
4. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates.
5. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out. If the guide has a "Set up via API" section, use it for anything MCP can't do before treating a step as UI-only.
6. Stop on any blocker and give me options. Approving this plan authorizes the writes it lists; pause only for writes beyond what's listed.

Build:
- Extend blueprint schema additively when upserting; don't remove or overwrite existing properties, and treat type conflicts as a blocker, not an auto-fix.
- Never print secret values into the chat or logs; ask me to set them in Port, or write them via the secrets API without echoing them back.
- List any mock data in the plan, minimal and labeled mock; once approved, seed it without re-asking, and tell me what you seeded.
- For anything the guide writes downstream (e.g. a webhook target), use a real entity, not a mock.
- For pages/widgets, use the real page identifier from the app URL, not a guessed slug.
- When you hit a UI step confirmed (not assumed) unsupported via MCP and not covered by the guide's API sections, pause, give exact clicks, then resume via MCP.
- Validate and give links after each meaningful step (only a tool-returned URL, no guessed paths); don't proceed if the last run wasn't a success.

Done:
- Run the guide's "Let's test it" steps where possible (e.g. execute a workflow test run) and confirm the expected output exists in Port.
- Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.

This guide demonstrates how to build a single workflow that sends Slack messages to users using Port's Slack app. The workflow covers five common notification scenarios from one graph: it listens for merged pull requests, new Jira issues, new PagerDuty incidents, and new platform requests, and it can also be invoked on demand, for example from another workflow when a deployment fails. In each case, it uses the email address from your Port integrations (such as GitHub, Jira, and PagerDuty) to look up the corresponding Slack user ID and send a targeted message to that user.

Common use cases

  • Pull request notifications: Notify reviewers when PRs are assigned or merged.
  • Jira notifications: Notify users when their Jira issues are assigned.
  • On-call handoffs: Notify the next person on-call when their shift is starting.
  • Approval requests: Notify approvers when their approval is needed for platform requests.
  • Deployment notifications: Alert developers when their deployments complete or fail.

Prerequisites

  • Port's Slack app installed in your workspace.
  • Access to the Slack app bot token (automatically created as a system secret).
  • The integrations for the notification sources you plan to keep: Port's GitHub Ocean integration for pull request notifications, and Port's Jira and PagerDuty integrations for issue and incident notifications.

How it works

The workflow has five triggers that all feed the same two-step core: look up the recipient's Slack ID by email, then send them a direct message.

NotificationFires whenRecipient
Pull request mergedA githubPullRequest entity is updated and its status is mergedThe PR creator
Jira issue assignedA jiraIssue entity is createdThe issue assignee
PagerDuty incidentA pagerdutyIncident entity is createdThe incident assignee
Approval requestA platformRequest entity is createdThe request approver
Failed deploymentThe self-service trigger runs, manually or invoked by another workflowThe email passed in the triggered_by_email input

When any trigger fires:

  1. The Get Slack user ID node extracts the recipient's email from whichever trigger fired and looks it up using Slack's users.lookupByEmail API.
  2. The Route by source condition node checks which source triggered the run and routes to a message node tailored to it, which sends the direct message using Slack's chat.postMessage API.
Five trigger nodes connected to a shared Get Slack user ID webhook node, followed by a condition node routing to five Slack message nodes

Build the workflow

  1. Go to the Workflows page in Port.

  2. Click on the + Workflow button in the top-right corner.

  3. Click on the {...} button in the top right corner.

  4. Copy and paste the workflow JSON below into the editor to replace the example workflow:

    Send Slack message to user workflow (Click to expand)
    {
    "identifier": "send_slack_message_to_user",
    "title": "Send Slack message to user",
    "icon": "Slack",
    "description": "Looks up a user's Slack ID by email and sends them a targeted message when a PR is merged, a Jira issue or PagerDuty incident is assigned, an approval is needed, or a deployment fails",
    "nodes": [
    {
    "identifier": "trigger_pr_merged",
    "title": "On PR Merged",
    "config": {
    "type": "EVENT_TRIGGER",
    "event": {
    "type": "ENTITY_UPDATED",
    "blueprintIdentifier": "githubPullRequest"
    },
    "condition": {
    "type": "JQ",
    "expressions": [
    ".diff.after.properties.status == \"merged\""
    ],
    "combinator": "and"
    }
    }
    },
    {
    "identifier": "trigger_jira_issue",
    "title": "On Jira Issue Created",
    "config": {
    "type": "EVENT_TRIGGER",
    "event": {
    "type": "ENTITY_CREATED",
    "blueprintIdentifier": "jiraIssue"
    }
    }
    },
    {
    "identifier": "trigger_pagerduty_incident",
    "title": "On PagerDuty Incident Created",
    "config": {
    "type": "EVENT_TRIGGER",
    "event": {
    "type": "ENTITY_CREATED",
    "blueprintIdentifier": "pagerdutyIncident"
    }
    }
    },
    {
    "identifier": "trigger_platform_request",
    "title": "On Platform Request Created",
    "config": {
    "type": "EVENT_TRIGGER",
    "event": {
    "type": "ENTITY_CREATED",
    "blueprintIdentifier": "platformRequest"
    }
    }
    },
    {
    "identifier": "trigger_manual",
    "title": "On Deployment Failure",
    "description": "Run manually or invoke from another workflow",
    "config": {
    "type": "SELF_SERVE_TRIGGER",
    "userInputs": {
    "properties": {
    "triggered_by_email": {
    "type": "string",
    "title": "Recipient email"
    },
    "identifier": {
    "type": "string",
    "title": "Service identifier"
    },
    "image": {
    "type": "string",
    "title": "Image"
    },
    "environment": {
    "type": "string",
    "title": "Environment"
    },
    "run_id": {
    "type": "string",
    "title": "Deployment run ID"
    }
    },
    "required": [
    "triggered_by_email",
    "identifier",
    "image",
    "environment",
    "run_id"
    ]
    },
    "published": true
    }
    },
    {
    "identifier": "get_slack_user_id",
    "title": "Get Slack user ID by email",
    "icon": "Slack",
    "config": {
    "type": "WEBHOOK",
    "url": "https://slack.com/api/users.lookupByEmail?email={{ .outputs.trigger.diff.after.relations.creator // .outputs.trigger.diff.after.properties.assignee // .outputs.trigger.diff.after.properties.approver // .outputs.trigger.triggered_by_email }}",
    "agent": false,
    "synchronized": true,
    "method": "GET",
    "headers": {
    "Content-Type": "application/x-www-form-urlencoded",
    "Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
    }
    }
    },
    {
    "identifier": "route_by_source",
    "title": "Route by Source",
    "config": {
    "type": "CONDITION",
    "outlets": [
    {
    "identifier": "pr_merged",
    "title": "PR Merged",
    "expression": "(.outputs.trigger.diff.after.blueprint // \"manual\") == \"githubPullRequest\""
    },
    {
    "identifier": "jira_issue",
    "title": "Jira Issue",
    "expression": "(.outputs.trigger.diff.after.blueprint // \"manual\") == \"jiraIssue\""
    },
    {
    "identifier": "pagerduty_incident",
    "title": "PagerDuty Incident",
    "expression": "(.outputs.trigger.diff.after.blueprint // \"manual\") == \"pagerdutyIncident\""
    },
    {
    "identifier": "platform_request",
    "title": "Platform Request",
    "expression": "(.outputs.trigger.diff.after.blueprint // \"manual\") == \"platformRequest\""
    },
    {
    "identifier": "deployment_failure",
    "title": "Deployment Failure",
    "expression": "(.outputs.trigger.diff.after.blueprint // \"manual\") == \"manual\""
    }
    ]
    }
    },
    {
    "identifier": "notify_pr_creator",
    "title": "Notify PR creator",
    "icon": "Slack",
    "config": {
    "type": "WEBHOOK",
    "url": "https://slack.com/api/chat.postMessage",
    "agent": false,
    "synchronized": true,
    "method": "POST",
    "headers": {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
    },
    "body": {
    "channel": "{{ .outputs.get_slack_user_id.response.user.id }}",
    "text": "🚀 Your pull request has been merged",
    "blocks": [
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Hi <@{{ .outputs.get_slack_user_id.response.user.id }}>,\n\nYour pull request *{{ .outputs.trigger.diff.after.title }}* has been merged! 🚀"
    }
    },
    {
    "type": "section",
    "fields": [
    {
    "type": "mrkdwn",
    "text": "*Repository:*\n{{ .outputs.trigger.diff.after.relations.repository }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Merged At:*\n{{ .outputs.trigger.diff.after.properties.mergedAt }}"
    }
    ]
    },
    {
    "type": "actions",
    "elements": [
    {
    "type": "button",
    "text": {
    "type": "plain_text",
    "text": "View PR"
    },
    "url": "{{ .outputs.trigger.diff.after.properties.link }}"
    }
    ]
    }
    ]
    }
    }
    },
    {
    "identifier": "notify_jira_assignee",
    "title": "Notify Jira assignee",
    "icon": "Slack",
    "config": {
    "type": "WEBHOOK",
    "url": "https://slack.com/api/chat.postMessage",
    "agent": false,
    "synchronized": true,
    "method": "POST",
    "headers": {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
    },
    "body": {
    "channel": "{{ .outputs.get_slack_user_id.response.user.id }}",
    "text": "🔔 You have been assigned a Jira issue",
    "blocks": [
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Hi <@{{ .outputs.get_slack_user_id.response.user.id }}>,\n\nYou have been assigned to a Jira issue:"
    }
    },
    {
    "type": "section",
    "fields": [
    {
    "type": "mrkdwn",
    "text": "*Issue:*\n{{ .outputs.trigger.diff.after.properties.title }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Status:*\n{{ .outputs.trigger.diff.after.properties.status }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Priority:*\n{{ .outputs.trigger.diff.after.properties.priority }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Assigned At:*\n{{ .outputs.trigger.diff.after.properties.updatedAt }}"
    }
    ]
    },
    {
    "type": "actions",
    "elements": [
    {
    "type": "button",
    "text": {
    "type": "plain_text",
    "text": "View Issue"
    },
    "url": "{{ .outputs.trigger.diff.after.properties.url }}"
    }
    ]
    }
    ]
    }
    }
    },
    {
    "identifier": "notify_pagerduty_assignee",
    "title": "Notify PagerDuty assignee",
    "icon": "Slack",
    "config": {
    "type": "WEBHOOK",
    "url": "https://slack.com/api/chat.postMessage",
    "agent": false,
    "synchronized": true,
    "method": "POST",
    "headers": {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
    },
    "body": {
    "channel": "{{ .outputs.get_slack_user_id.response.user.id }}",
    "text": "🚨 You have been assigned a new PagerDuty incident!",
    "blocks": [
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Hi <@{{ .outputs.get_slack_user_id.response.user.id }}>,\n\nYou have been assigned to a new PagerDuty incident:\n*Incident:* {{ .outputs.trigger.diff.after.properties.title }}\n*Priority:* {{ .outputs.trigger.diff.after.properties.priority }}\n*Service:* {{ .outputs.trigger.diff.after.properties.service }}"
    }
    },
    {
    "type": "actions",
    "elements": [
    {
    "type": "button",
    "text": {
    "type": "plain_text",
    "text": "View Incident"
    },
    "url": "https://app.port.io/pagerdutyIncident/{{ .outputs.trigger.diff.after.identifier }}"
    }
    ]
    }
    ]
    }
    }
    },
    {
    "identifier": "notify_approver",
    "title": "Notify approver",
    "icon": "Slack",
    "config": {
    "type": "WEBHOOK",
    "url": "https://slack.com/api/chat.postMessage",
    "agent": false,
    "synchronized": true,
    "method": "POST",
    "headers": {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
    },
    "body": {
    "channel": "{{ .outputs.get_slack_user_id.response.user.id }}",
    "text": "✅ Approval request",
    "blocks": [
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Hi <@{{ .outputs.get_slack_user_id.response.user.id }}>,\n\nA new platform request requires your approval:"
    }
    },
    {
    "type": "section",
    "fields": [
    {
    "type": "mrkdwn",
    "text": "*Request:*\n{{ .outputs.trigger.diff.after.properties.title }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Type:*\n{{ .outputs.trigger.diff.after.properties.requestType }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Requester:*\n{{ .outputs.trigger.diff.after.properties.requester }}"
    },
    {
    "type": "mrkdwn",
    "text": "*Created:*\n{{ .outputs.trigger.diff.after.properties.createdAt }}"
    }
    ]
    },
    {
    "type": "actions",
    "elements": [
    {
    "type": "button",
    "text": {
    "type": "plain_text",
    "text": "Review Request"
    },
    "url": "https://app.port.io/platformRequest/{{ .outputs.trigger.diff.after.identifier }}"
    }
    ]
    }
    ]
    }
    }
    },
    {
    "identifier": "notify_deployment_user",
    "title": "Notify user of failed deployment",
    "icon": "Slack",
    "config": {
    "type": "WEBHOOK",
    "url": "https://slack.com/api/chat.postMessage",
    "agent": false,
    "synchronized": true,
    "method": "POST",
    "headers": {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
    },
    "body": {
    "channel": "{{ .outputs.get_slack_user_id.response.user.id }}",
    "text": "🚨 Your deployment has failed.",
    "blocks": [
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Hi <@{{ .outputs.get_slack_user_id.response.user.id }}>,\n\nYour deployment of service *{{ .outputs.trigger.identifier }}* (image: {{ .outputs.trigger.image }}) to environment *{{ .outputs.trigger.environment }}* has failed."
    }
    },
    {
    "type": "section",
    "fields": [
    {
    "type": "mrkdwn",
    "text": "*Run ID:*\n{{ .outputs.trigger.run_id }}"
    }
    ]
    }
    ]
    }
    }
    }
    ],
    "connections": [
    {
    "sourceIdentifier": "trigger_pr_merged",
    "targetIdentifier": "get_slack_user_id"
    },
    {
    "sourceIdentifier": "trigger_jira_issue",
    "targetIdentifier": "get_slack_user_id"
    },
    {
    "sourceIdentifier": "trigger_pagerduty_incident",
    "targetIdentifier": "get_slack_user_id"
    },
    {
    "sourceIdentifier": "trigger_platform_request",
    "targetIdentifier": "get_slack_user_id"
    },
    {
    "sourceIdentifier": "trigger_manual",
    "targetIdentifier": "get_slack_user_id"
    },
    {
    "sourceIdentifier": "get_slack_user_id",
    "targetIdentifier": "route_by_source"
    },
    {
    "sourceIdentifier": "route_by_source",
    "targetIdentifier": "notify_pr_creator",
    "sourceOutletIdentifier": "pr_merged"
    },
    {
    "sourceIdentifier": "route_by_source",
    "targetIdentifier": "notify_jira_assignee",
    "sourceOutletIdentifier": "jira_issue"
    },
    {
    "sourceIdentifier": "route_by_source",
    "targetIdentifier": "notify_pagerduty_assignee",
    "sourceOutletIdentifier": "pagerduty_incident"
    },
    {
    "sourceIdentifier": "route_by_source",
    "targetIdentifier": "notify_approver",
    "sourceOutletIdentifier": "platform_request"
    },
    {
    "sourceIdentifier": "route_by_source",
    "targetIdentifier": "notify_deployment_user",
    "sourceOutletIdentifier": "deployment_failure"
    }
    ]
    }
  5. Click Save to save the workflow.

Replace your Slack bot token

The workflow references the bot token secret as __SLACK_APP_BOT_TOKEN_T123. To find your secret's actual name:

  1. Open the Credentials modal and click on the Secrets tab.
  2. Look for a secret named __SLACK_APP_BOT_TOKEN_<team_id> where <team_id> is your Slack workspace ID (e.g. T0206G5RTJQ), and replace __SLACK_APP_BOT_TOKEN_T123 with it in every node that uses it.

Customize the workflow

Keep only the sources you use

If one of the blueprints does not exist in Port (for example platformRequest, which is a custom blueprint), remove its trigger node, its condition outlet, its message node, and their connections before publishing the workflow.

Adjust the email lookup

The get_slack_user_id node builds the recipient's email with a single jq expression that returns the first non-empty value among:

  • .outputs.trigger.diff.after.relations.creator - the PR creator (GitHub).
  • .outputs.trigger.diff.after.properties.assignee - the Jira or PagerDuty assignee.
  • .outputs.trigger.diff.after.properties.approver - the platform request approver.
  • .outputs.trigger.triggered_by_email - the input of the self-service trigger.

Only the trigger that fired produces outputs, so the other paths resolve to null and jq's // operator skips them. If your blueprints store emails under different properties, update these paths in the lookup node's url.

Add a new source

To notify users from another source:

  1. Add an event trigger node for the blueprint and connect it to the get_slack_user_id node.
  2. Append the path of the property holding the recipient's email to the lookup node's // chain.
  3. Add an outlet for the blueprint to the route_by_source condition node.
  4. Add a message node with the Slack blocks you want, and connect it to the new condition outlet.

Send from another workflow

The self-service trigger doubles as an API entry point, so any workflow can send a Slack message by invoking this one. For example, to notify a user when their deployment fails, add an asynchronous webhook node to your deployment workflow's failure branch:

Invoke the notification workflow on failure (Click to expand)
{
"identifier": "notify_on_deploy_failure",
"title": "Notify on Deploy Failure",
"config": {
"type": "WEBHOOK",
"method": "POST",
"synchronized": false,
"url": "https://api.port.io/v1/workflows/send_slack_message_to_user/runs",
"body": {
"inputs": {
"triggered_by_email": "{{ .workflowRun.trigger.by.email }}",
"identifier": "{{ .outputs.trigger.service }}",
"image": "{{ .outputs.trigger.image }}",
"environment": "{{ .outputs.trigger.environment }}",
"run_id": "{{ .workflowRun.identifier }}"
}
}
}
}

Connect this node from your deployment condition's failure branch, and adjust the field paths (.outputs.trigger.service, .outputs.trigger.image, and so on) to match your deployment workflow's actual trigger inputs. The trigger a workflow run API starts the run from the workflow's self-service trigger.

If your deployment workflow does not have a failure branch yet, see the Promote to production workflow guide for a full example of branching on deployment status with a CONDITION node and onFailure.

Let's test it

  1. Test the failed deployment branch from the builder: click Execute workflow, choose the On Deployment Failure trigger when prompted (workflows with multiple triggers ask which trigger to run the test from), and fill in test values. You can also run it from the Self-service page.
  2. Test the event branches by creating or updating a matching entity, for example updating a githubPullRequest entity's status to merged or creating a jiraIssue entity. Event branches can't be started from the builder's test button, which currently requires a self-service trigger.
  3. Go to the Workflows page, open the workflow, and check the Runs tab for the latest run.
  4. Verify that the Slack message was sent to the intended user, and that the condition routed to the branch matching the source.
  5. If a run failed, expand the failing node in the run details to see its output and logs. See Track & manage runs for more details.