Skip to main content

Check out Port for yourself ➜ 

Promote to production using workflows

This guide walks you through using Port's AI assistant to build a Promote to Production workflow. By the end, you will have a self-service workflow that developers can trigger to safely deploy services to production, with built-in health checks, deployment tracking, and team notifications.

Prerequisites

This guide assumes the following:

Beta feature

Port workflows are currently in closed beta. Workflows may undergo breaking changes and occasional downtime without prior notice.

Build the workflow

We will build the workflow using Port's AI assistant. Follow the steps below to build the workflow:

  1. Go to the Workflows page of your portal.

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

  3. You will see a dialog where you can describe the workflow you want to build:

  4. Describe the workflow to the AI, in the text area, paste the following prompt:

    Prompt (click to expand)
    Create a workflow called "Promote to Production" with:
    - Self-service trigger for the githubRepository blueprint
    - User inputs: service (entity from githubRepository), version (string), skip_health_check (boolean)
    - Fetch service details from Port API
    - Condition node to decide whether to run health check based on skip_health_check input
    - If health check is needed, perform a webhook call to validate service health
    - If unhealthy, send a Slack notification and stop
    - Deploy to production by triggering a GitHub Actions workflow (deploy-production.yml) using the GitHub integration action
    - On success: create a dora_deployment entity and send a success Slack notification
    - On failure: create a PagerDuty incident via the PagerDuty Events API v2 and send a failure Slack notification
    - All paths should converge to a final UPSERT_ENTITY node that updates the run status
    Review changes

    The AI will generate the workflow structure. Click Review changes to inspect it, then click Apply to load it into the editor. You can also review the workflow in the Workflows page of your portal.

  5. Review the generated workflow and replace placeholder values with your actual values (see Configure the workflow below).

  6. Click Publish in the top-right corner of the editor. If you encounter validation errors, refer to the Troubleshooting section.

Configure the workflow

After publishing, you need to replace placeholder values in the workflow nodes.

Add secrets to Port

  1. Go to your portal's Settings page.

  2. Navigate to Credentials and add the following secrets:

Configure the GitHub integration action

In the deploy_to_production node, set the installationId to your GitHub integration's installation ID. You can find this in the Data sources page of your portal. Update the org field to your GitHub organization name.

Configure Slack webhooks

Update the webhook URL in each of the three Slack notification nodes (notify_health_check_failed, notify_deployment_failed, notify_deployment_success) with your actual Slack incoming webhook URL.

Workflow reference

Below is the corrected workflow JSON with the GitHub integration action and PagerDuty Events API. Use this as a reference to verify your AI-generated workflow has the correct node configurations.

Full workflow JSON (click to expand)
{
"identifier": "promote_to_production",
"title": "Promote to Production",
"icon": "Deployment",
"description": "Deploy service to production with safety checks, GitHub Actions deployment, and team notifications",
"allowAnyoneToViewRuns": true,
"nodes": [
{
"identifier": "check_deployment_status",
"title": "Check Deployment Status",
"icon": "Merge",
"description": "Evaluate if deployment succeeded or failed",
"config": {
"type": "CONDITION",
"options": [
{
"identifier": "success",
"title": "Deployment Succeeded",
"expression": ".outputs.wait_for_deployment.workflow_conclusion == \"success\""
},
{
"identifier": "failure",
"title": "Deployment Failed",
"expression": ".outputs.wait_for_deployment.workflow_conclusion != \"success\""
}
]
},
"variables": {}
},
{
"identifier": "create_incident_ticket",
"title": "Create Incident Ticket",
"icon": "Pagerduty",
"description": "Create PagerDuty incident for failed deployment",
"config": {
"type": "WEBHOOK",
"url": "https://events.pagerduty.com/v2/enqueue",
"agent": false,
"synchronized": true,
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"payload": {
"source": "port-workflow",
"summary": "Production Deployment Failed: {{ .inputs.service }} v{{ .inputs.version }}",
"severity": "critical",
"component": "{{ .inputs.service }}",
"custom_details": {
"service": "{{ .inputs.service }}",
"version": "{{ .inputs.version }}",
"port_run_id": "{{ .run.id }}",
"triggered_by": "{{ .trigger.by.user.email }}"
}
},
"routing_key": "{{ .secrets.PAGERDUTY_ROUTING_KEY }}",
"event_action": "trigger"
}
},
"variables": {
"dedup_key": "{{ .response.data.dedup_key }}"
}
},
{
"identifier": "deploy_to_production",
"title": "Deploy to Production",
"icon": "Github",
"description": "Trigger GitHub Actions deployment workflow",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<Installation ID>",
"integrationProvider": "GITHUB",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "<GitHub Organization>",
"repo": "{{ .inputs.service }}",
"workflow": "deploy-production-workflow.yml",
"workflowInputs": {
"service": "{{ .inputs.service }}",
"version": "{{ .inputs.version }}",
"port_run_id": "{{ .run.id }}",
"triggered_by": "{{ .trigger.by.user.email }}"
},
"reportWorkflowStatus": true
}
},
"variables": {}
},
{
"identifier": "evaluate_health",
"title": "Evaluate Health",
"icon": "Merge",
"description": "Check if service is healthy enough to deploy",
"config": {
"type": "CONDITION",
"options": [
{
"identifier": "healthy",
"title": "Healthy",
"expression": ".outputs.perform_health_check.response.data.status == \"healthy\""
},
{
"identifier": "unhealthy",
"title": "Unhealthy",
"expression": ".outputs.perform_health_check.response.data.status != \"healthy\""
}
]
},
"variables": {}
},
{
"identifier": "notify_deployment_failed",
"title": "Notify Deployment Failed",
"icon": "Slack",
"description": "Alert team of deployment failure",
"config": {
"type": "WEBHOOK",
"url": "https://hooks.slack.com/services/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",
"agent": false,
"synchronized": true,
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"text": "Production Deployment Failed",
"blocks": [
{
"text": {
"text": "Production Deployment Failed",
"type": "plain_text"
},
"type": "header"
},
{
"type": "section",
"fields": [
{
"text": "*Service:*\n{{ .inputs.service }}",
"type": "mrkdwn"
},
{
"text": "*Version:*\n{{ .inputs.version }}",
"type": "mrkdwn"
},
{
"text": "*Deployed by:*\n{{ .trigger.by.user.email }}",
"type": "mrkdwn"
},
{
"text": "*Status:*\nFailed",
"type": "mrkdwn"
}
]
},
{
"text": {
"text": "A PagerDuty incident has been created automatically.",
"type": "mrkdwn"
},
"type": "section"
}
]
}
},
"variables": {}
},
{
"identifier": "notify_deployment_success",
"title": "Notify Deployment Success",
"icon": "Slack",
"description": "Celebrate successful deployment",
"config": {
"type": "WEBHOOK",
"url": "https://hooks.slack.com/services/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",
"agent": false,
"synchronized": true,
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"text": "Production Deployment Successful",
"blocks": [
{
"text": {
"text": "Production Deployment Successful",
"type": "plain_text"
},
"type": "header"
},
{
"type": "section",
"fields": [
{
"text": "*Service:*\n{{ .inputs.service }}",
"type": "mrkdwn"
},
{
"text": "*Version:*\n{{ .inputs.version }}",
"type": "mrkdwn"
},
{
"text": "*Deployed by:*\n{{ .trigger.by.user.email }}",
"type": "mrkdwn"
},
{
"text": "*Time:*\n{{ now | date \"2006-01-02 15:04:05\" }}",
"type": "mrkdwn"
}
]
}
]
}
},
"variables": {}
},
{
"identifier": "notify_health_check_failed",
"title": "Notify Health Check Failed",
"icon": "Slack",
"description": "Alert team that deployment was blocked by health check",
"config": {
"type": "WEBHOOK",
"url": "https://hooks.slack.com/services/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",
"agent": false,
"synchronized": true,
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"text": "Production Deployment Blocked",
"blocks": [
{
"text": {
"text": "Production Deployment Blocked",
"type": "plain_text"
},
"type": "header"
},
{
"type": "section",
"fields": [
{
"text": "*Service:*\n{{ .inputs.service }}",
"type": "mrkdwn"
},
{
"text": "*Version:*\n{{ .inputs.version }}",
"type": "mrkdwn"
},
{
"text": "*Reason:*\nHealth check failed",
"type": "mrkdwn"
},
{
"text": "*Triggered by:*\n{{ .trigger.by.user.email }}",
"type": "mrkdwn"
}
]
}
]
}
},
"variables": {}
},
{
"identifier": "perform_health_check",
"title": "Perform Health Check",
"icon": "Health",
"description": "Validate service health before deployment",
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/health-check",
"agent": false,
"synchronized": true,
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"checks": [
"dependencies",
"database",
"external_apis"
],
"service": "{{ .inputs.service }}"
}
},
"variables": {}
},
{
"identifier": "should_run_health_check",
"title": "Health Check Decision",
"icon": "Merge",
"description": "Determine if health check should run",
"config": {
"type": "CONDITION",
"options": [
{
"identifier": "run_check",
"title": "Run Health Check",
"expression": ".inputs.skip_health_check == false"
},
{
"identifier": "skip_check",
"title": "Skip Health Check",
"expression": ".inputs.skip_health_check == true"
}
]
},
"variables": {}
},
{
"identifier": "trigger",
"title": "Promote to Production",
"icon": "Rocket",
"description": "Developer initiates production deployment",
"config": {
"type": "SELF_SERVE_TRIGGER",
"userInputs": {
"properties": {
"service": {
"title": "Service",
"description": "Select the service to deploy",
"type": "string",
"format": "entity",
"blueprint": "githubRepository"
},
"version": {
"title": "Version/Tag",
"description": "Version or git tag to deploy (e.g., v1.2.3 or main)",
"type": "string"
},
"skip_health_check": {
"title": "Skip Health Check",
"description": "Skip pre-deployment health validation",
"type": "boolean",
"default": false
}
},
"required": [
"service",
"version"
]
},
"actionCardButtonText": "Deploy",
"executeActionButtonText": "Deploy Now"
},
"variables": {}
},
{
"identifier": "wait_for_deployment",
"title": "Wait for Deployment",
"icon": "Clock",
"description": "Check GitHub workflow status",
"config": {
"type": "WEBHOOK",
"url": "https://api.github.com/repos/<GitHub Organization>/{{ .inputs.service }}/actions/runs?per_page=1",
"agent": false,
"synchronized": true,
"method": "GET",
"headers": {
"Accept": "application/vnd.github.v3+json",
"Authorization": "Bearer {{ .secrets.GITHUB_TOKEN }}"
}
},
"variables": {
"workflow_url": "{{ .response.data.workflow_runs[0].html_url }}",
"workflow_conclusion": "{{ .response.data.workflow_runs[0].conclusion }}"
}
}
],
"connections": [
{
"description": null,
"sourceIdentifier": "trigger",
"targetIdentifier": "should_run_health_check"
},
{
"description": null,
"sourceIdentifier": "should_run_health_check",
"targetIdentifier": "perform_health_check",
"sourceOptionIdentifier": "run_check"
},
{
"description": null,
"sourceIdentifier": "should_run_health_check",
"targetIdentifier": "deploy_to_production",
"sourceOptionIdentifier": "skip_check"
},
{
"description": null,
"sourceIdentifier": "should_run_health_check",
"targetIdentifier": "deploy_to_production",
"fallback": true
},
{
"description": null,
"sourceIdentifier": "perform_health_check",
"targetIdentifier": "evaluate_health"
},
{
"description": null,
"sourceIdentifier": "evaluate_health",
"targetIdentifier": "deploy_to_production",
"sourceOptionIdentifier": "healthy"
},
{
"description": null,
"sourceIdentifier": "evaluate_health",
"targetIdentifier": "notify_health_check_failed",
"sourceOptionIdentifier": "unhealthy"
},
{
"description": null,
"sourceIdentifier": "deploy_to_production",
"targetIdentifier": "wait_for_deployment"
},
{
"description": null,
"sourceIdentifier": "wait_for_deployment",
"targetIdentifier": "check_deployment_status"
},
{
"description": null,
"sourceIdentifier": "check_deployment_status",
"targetIdentifier": "notify_deployment_success",
"sourceOptionIdentifier": "success"
},
{
"description": null,
"sourceIdentifier": "check_deployment_status",
"targetIdentifier": "create_incident_ticket",
"sourceOptionIdentifier": "failure"
},
{
"description": null,
"sourceIdentifier": "create_incident_ticket",
"targetIdentifier": "notify_deployment_failed"
}
]
}

Key differences from what the AI may generate:

  • The deploy_to_production node uses INTEGRATION_ACTION with your GitHub integration instead of a raw webhook. See the GitHub integration action docs for details.
  • The create_incident_ticket node calls the PagerDuty Events API v2 directly to create a real PagerDuty incident, rather than just creating a Port entity. Your PagerDuty integration will sync the incident back to Port automatically.
  • The notify_deployment_failed Slack message references the PagerDuty incident instead of a Port entity link.

Create the GitHub workflow

Your repositories need a GitHub Actions workflow file that this Port workflow will trigger. Create the following file in each repository that will use this workflow:

deploy-production.yml template (click to expand)
name: Deploy to Production

on:
workflow_dispatch:
inputs:
version:
description: "Version/tag to deploy"
required: true
type: string
service:
description: "Service name"
required: true
type: string
triggered_by:
description: "User who triggered deployment"
required: true
type: string
port_run_id:
description: "Port workflow run ID"
required: true
type: string

jobs:
deploy:
runs-on: ubuntu-latest
environment: production

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.version }}

- name: Deploy to production
run: |
echo "Deploying ${{ inputs.service }} version ${{ inputs.version }}"
echo "Triggered by: ${{ inputs.triggered_by }}"
echo "Port Run ID: ${{ inputs.port_run_id }}"

# Replace with your actual deployment commands:
# kubectl apply -f k8s/
# helm upgrade --install ...
# aws ecs update-service ...

- name: Verify deployment
run: |
echo "Verifying deployment..."
# Add health check or smoke test commands here
Customize the deployment steps

Customize the deployment steps for your infrastructure (Kubernetes, AWS ECS, Helm, Terraform, etc.).

Let's test it !

Before using this in production, run through these test scenarios:

Test 1: successful deployment

  1. Go to the Self-service page of your portal.
  2. Find Promote to Production and click on it.
  3. Select a service, enter a valid version/tag, and check Skip Health Check.
  4. Click Execute.
  5. Verify that:
    • The GitHub Actions workflow runs and succeeds.
    • A dora_deployment entity is created in your catalog.
    • A success notification is sent to your Slack channel.

Test 2: failed deployment

  1. Trigger the workflow with an invalid version (e.g., nonexistent-tag).
  2. Verify that:
    • The GitHub Actions workflow fails.
    • A real PagerDuty incident is created with critical severity.
    • The PagerDuty integration syncs the incident back to Port as a pagerdutyIncident entity.
    • A failure notification is sent to your Slack channel.

Test 3: health check failure

  1. Trigger the workflow with Skip Health Check unchecked.
  2. If your health check endpoint returns an unhealthy status, verify that:
    • The workflow stops before deployment.
    • A Slack notification is sent about the health check failure.

Debugging your workflow

When building and testing workflows, understanding how to inspect execution data will help you identify and resolve issues quickly.

Capture webhook responses with variables

By default, webhook node outputs include the full response at .outputs["node_id"].response.data. To extract and persist specific fields from a webhook response, define variables on the node. Variables are evaluated using .response.data within the same node:

{
"identifier": "create_incident",
"config": {
"type": "WEBHOOK",
"url": "https://events.pagerduty.com/v2/enqueue",
"method": "POST",
"body": { ... }
},
"variables": {
"dedup_key": "{{ .response.data.dedup_key }}",
"status": "{{ .response.data.status }}"
}
}

Subsequent nodes can then reference these values as {{ .outputs["create_incident"].dedup_key }}. See the data flow docs for more details.

Variables replace default outputs

When you define variables on a node, the default outputs (like response.data) are replaced entirely. If you need both custom variables and the raw response, include the response explicitly in your variables.

Use the workflow runs audit log

Every workflow execution is tracked in the Workflow runs tab. When a run fails:

  1. Open the failed run from the Runs tab.
  2. Look for nodes with a FAILED status in the node runs list.
  3. Expand the node to see its output and logs, including HTTP status codes and error messages from external APIs.
Faster debugging

Focus on the first node in the chain that shows a FAILED status. Downstream failures are often caused by the first failing node passing unexpected data.

Verify node inputs and outputs

When a node produces unexpected results, check:

  • Outputs: Each node run shows the output data it produced. Verify that the response contains the fields you expect.
  • Expressions: If a condition node routes incorrectly, check that the expression references the correct output path (e.g., .outputs["wait_for_deployment"].workflow_conclusion vs .outputs["wait_for_deployment"].response.data.workflow_runs[0].conclusion).
  • Variables: If you defined variables, verify they correctly extract the fields you need. Remember that variables use .response.data (the current node's raw response), while subsequent nodes use .outputs["node_id"].

Troubleshooting

Below are common issues you may encounter when building or publishing this workflow.

IssueCauseSolution
Invalid input → at nodes[X].config.typeA WEBHOOK node is the last node in a path (terminal position).Add a final UPSERT_ENTITY node and connect all terminal paths to it.
Invalid input → at nodes[X].config.type (on webhook)The node has "synchronized": false.Change to "synchronized": true.
AI uses wrong blueprint identifierThe AI may guess githubRepo instead of githubRepository.Verify blueprint identifiers in your Builder page and correct them.
AI uses invalid icon namesIcons like GitBranch or Split do not exist in Port's icon catalog.Use valid icons: Rocket, Port, Github, Slack, Health, Clock, Pagerduty, Merge, Deployment.
GitHub workflow not triggeringWrong installation ID or repo path.Verify the installationId in the Data sources page and ensure the repo field uses the correct org/repo format.
PagerDuty incident not createdWrong routing key or missing secret.Verify PAGERDUTY_ROUTING_KEY is added to Port secrets and matches the integration key from your PagerDuty service.
Slack notifications not sendingPlaceholder webhook URL not replaced.Replace all instances of https://hooks.slack.com/services/YOUR/WEBHOOK/URL with your actual webhook URL.
Entities not being createdBlueprint identifier mismatch or missing required properties.Verify blueprint identifiers and required properties in your Builder page.

Next steps

  • Customize the Slack notification messages to match your team's communication style.
  • Add an approval step before production deployments for additional safety.
  • Create a dashboard to visualize deployment frequency, success rates, and incident trends.
  • Explore more workflow examples for inspiration.