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

Check out Port for yourself ➜ 

Manage developer environments

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/manage-developer-environments

Read the raw markdown version at https://docs.port.io/guides/all/manage-developer-environments.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 provides a step-by-step walkthrough for managing the full lifecycle of a developer environment with Port workflows.

We will build three workflows that cover the lifecycle end to end:

  1. Create provisions an environment from a pre-approved catalog template. It dispatches a GitHub Actions pipeline, then records the environment in the catalog with a TTL (time-to-live) so it terminates automatically instead of lingering forever.
  2. Extend adds time to an environment that is still in use. A 1-day or 3-day extension applies immediately, a 7-day extension routes to a team lead for approval first.
  3. Delete tears the environment down and marks it Deleted in the catalog, preserving the audit trail.

Each workflow dispatches a pipeline before it touches the catalog, so Port only records what the pipeline actually did.

Because every self-service workflow is automatically exposed as a tool through the Port MCP server, the same flows work without any UI at all: agents discover them with list_self_service_triggers and execute them with trigger_run.

Prerequisites

  1. Port's GitHub integration is installed. The workflows use it to dispatch GitHub Actions workflows.
  2. A GitHub repository to hold the pipeline files we create below.
Self-hosted GitHub integrations

If you run the GitHub integration yourself (Helm or Docker) rather than hosting it in Port, Actions processing is disabled by default. Enable it, or the dispatch nodes will silently fail to trigger anything.

1. Model developer environments in Port

We need two blueprints: a catalog of approved templates, and the environments themselves. Create them in this order, because the environment blueprint declares a relation to the catalog and Port rejects a relation whose target does not exist yet.

For each blueprint below:

  1. Navigate to the Data model page.

  2. Click + Blueprint.

  3. Click {...} Edit JSON.

  4. Copy and paste the JSON configuration into the editor.

  5. Click Save to create the blueprint.

Cloud resource catalog

This blueprint holds the pre-approved templates a developer can pick from. The create workflow filters it down to entries whose resource_type is Developer Environment.

Cloud resource catalog blueprint (Click to expand)
cloud-resource-catalog-blueprint.json
{
"identifier": "cloudResourceCatalog",
"description": "This blueprint represents a catalog of pre-approved, reusable resource templates",
"title": "Cloud Resource Catalog",
"icon": "DefaultProperty",
"schema": {
"properties": {
"resource_type": {
"type": "string",
"title": "Resource Type",
"description": "The kind of resource this catalog entry provisions"
},
"cost_per_month": {
"type": "number",
"title": "Monthly Cost ($)"
},
"description": {
"type": "string",
"title": "Description"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}

Add at least one entry with resource_type set to Developer Environment so it shows up as a selectable template when creating an environment.

Developer environment

Developer environment blueprint (Click to expand)
developer-env-blueprint.json
{
"identifier": "developerEnv",
"title": "Developer Env",
"icon": "DeployedAt",
"ownership": {
"type": "Direct"
},
"schema": {
"properties": {
"ttl": {
"type": "string",
"title": "TTL",
"format": "timer",
"description": "The self-termination date of the environment"
},
"deploymentStatus": {
"type": "string",
"icon": "Git",
"title": "Status",
"default": "Deployed",
"enum": ["Deployed", "Deploying", "Deleted", "Failed"],
"enumColors": {
"Deploying": "orange",
"Deployed": "green",
"Failed": "red",
"Deleted": "darkGray"
}
},
"reason": {
"icon": "Siren",
"type": "string",
"title": "Reason"
}
},
"required": []
},
"mirrorProperties": {
"requestor": {
"title": "Requestor",
"path": "owner.email"
}
},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {
"owner": {
"title": "Owner",
"target": "_user",
"required": false,
"many": false
},
"catalog_env": {
"title": "Catalog Entry",
"target": "cloudResourceCatalog",
"required": false,
"many": false
}
}
}

requestor is a mirror property that reads owner.email off the related _user entity. It isn't set directly, so the create workflow assigns ownership through the owner relation instead, and requestor resolves automatically. The ttl property uses Port's timer format, so Port tracks its expiration automatically and flags it as Expired once the date passes. catalog_env relates each environment to the template it was created from.

Verify the owner relation target

The create workflow sets relations.owner to the triggering user's email, which assumes _user entities in your organization are identified by email (Port's default). Confirm this matches your org before relying on it.

This blueprint carries only what the three workflows touch. You can enrich it later, for example with calculation properties that build a link to the running environment or to its cost dashboard.

2. Add the GitHub Actions pipelines

Each workflow dispatches a pipeline that does the real infrastructure work. Create the following three files in your repository under .github/workflows/.

Dedicated Workflows Repository

We recommend creating a dedicated repository for the workflows that are used by Port actions.

provision-dev-env.yml (Click to expand)
provision-dev-env.yml
name: Provision developer environment

on:
workflow_dispatch:
inputs:
env_name:
description: "Identifier of the environment to provision"
required: true
template:
description: "Catalog template to provision from"
required: true
ttl:
description: "Requested lifespan of the environment"
required: true
requested_by:
description: "User who requested the environment"
required: false

jobs:
provision:
runs-on: ubuntu-latest
steps:
- name: Provision environment
run: |
echo "Provisioning ${{ inputs.env_name }} from template ${{ inputs.template }}"
echo "Lifespan: ${{ inputs.ttl }}, requested by ${{ inputs.requested_by }}"

# Replace with your real provisioning commands, for example:
# terraform apply -auto-approve -var="name=${{ inputs.env_name }}"
# helm upgrade --install ${{ inputs.env_name }} ./charts/dev-env
extend-dev-env-ttl.yml (Click to expand)
extend-dev-env-ttl.yml
name: Extend developer environment TTL

on:
workflow_dispatch:
inputs:
env_name:
description: "Identifier of the environment to extend"
required: true
extension:
description: "Amount of time to add to the environment's lease"
required: true

jobs:
extend:
runs-on: ubuntu-latest
steps:
- name: Extend environment lease
run: |
echo "Extending ${{ inputs.env_name }} by ${{ inputs.extension }}"

# Replace with your real lease-extension commands, for example:
# kubectl annotate namespace ${{ inputs.env_name }} reaper/expiry=...
teardown-dev-env.yml (Click to expand)
teardown-dev-env.yml
name: Tear down developer environment

on:
workflow_dispatch:
inputs:
env_name:
description: "Identifier of the environment to tear down"
required: true
reason:
description: "Why the environment is being torn down"
required: false

jobs:
teardown:
runs-on: ubuntu-latest
steps:
- name: Tear down environment
run: |
echo "Tearing down ${{ inputs.env_name }}"
echo "Reason: ${{ inputs.reason }}"

# Replace with your real teardown commands, for example:
# terraform destroy -auto-approve -var="name=${{ inputs.env_name }}"
Replace the placeholder commands

Each pipeline only echoes its inputs, so you can verify the whole flow before wiring up real infrastructure. Swap the commented-out commands for your actual provisioning logic.

3. Build the workflows

We build one workflow per operation, so each stays small enough to read on a single canvas:

WorkflowTrigger surfaceWhat it does
create_developer_environmentSelf-service page, and the developerEnv create flow.Dispatches provision-dev-env.yml, then adds the environment to the catalog.
extend_developer_environment_ttlSelf-service page, and the entity bolt (⚡) menu.Dispatches extend-dev-env-ttl.yml, then pushes the ttl timer out. A 7-day extension needs team lead approval first.
delete_developer_environmentSelf-service page, and the entity bolt (⚡) menu.Dispatches teardown-dev-env.yml, then sets the environment's status to Deleted.

Each workflow's title, description, and input descriptions double as its tool definition for AI agents, so they are written as instructions an agent can follow. See expose workflows as tools for the full guidelines.

All three share a few conventions:

  • They set the same category, which is what groups their triggers together in an entity's bolt (⚡) menu even though they are separate workflows.
  • User inputs are referenced with {{ .outputs.trigger.<input> }}. See data flow for details.
  • Each dispatch node sets reportWorkflowStatus: true, so it waits for the GitHub Actions run to finish. The catalog node only runs after the pipeline succeeds.
Replace the variables
  • <YOUR_GITHUB_OCEAN_INTEGRATION_ID> - the identifier Port gave your GitHub integration, shown on the Data sources page and usually github-ocean. This is Port's identifier for the data source, not GitHub's numeric App installation ID.
  • <GITHUB_ORG> - your GitHub organization or user name.
  • <GITHUB_REPO> - the repository holding the three pipeline files.
  • <TEAM_LEAD_EMAIL> - the email address that should approve 7-day extensions. The responders.users list can hold more than one address.

Create a developer environment

The trigger collects a catalog template, a name, an owning team, and a TTL. provision_env dispatches the pipeline, then upsert_new_env writes the entity.

Create workflow JSON (Click to expand)
create-developer-environment.json
{
"identifier": "create_developer_environment",
"title": "Create a developer environment",
"icon": "DeployedAt",
"category": "Developer Environments",
"description": "Provision a developer environment from a pre-approved catalog template. Use when a user asks to create or spin up a dev environment.",
"nodes": [
{
"identifier": "create_env_trigger",
"title": "Create a developer environment",
"icon": "DeployedAt",
"config": {
"type": "SELF_SERVE_TRIGGER",
"contexts": [
{ "on": "CREATE_ENTITY", "blueprintIdentifier": "developerEnv" }
],
"userInputs": {
"properties": {
"catalog_env": {
"type": "string",
"format": "entity",
"blueprint": "cloudResourceCatalog",
"title": "Developer environment type",
"description": "Select a developer environment template from the cloud resource catalog.",
"dataset": {
"combinator": "and",
"rules": [
{ "property": "resource_type", "operator": "=", "value": "Developer Environment" }
]
}
},
"name": {
"type": "string",
"title": "Name",
"description": "The name of the developer environment. Use lowercase letters, digits, and hyphens, for example 'team-dev-env'."
},
"team": {
"type": "string",
"format": "entity",
"blueprint": "_team",
"title": "Owning team"
},
"ttl": {
"type": "string",
"title": "TTL",
"description": "When this environment should terminate. Accepted values: 1 day, 3 days, 7 days, 10 days.",
"default": "10 days",
"enum": ["1 day", "3 days", "7 days", "10 days"],
"enumColors": {
"1 day": "purple",
"3 days": "purple",
"7 days": "pink",
"10 days": "red"
}
}
},
"required": ["name", "ttl", "team", "catalog_env"],
"order": ["catalog_env", "ttl", "name", "team"]
},
"published": true
}
},
{
"identifier": "provision_env",
"title": "Provision the environment",
"icon": "Github",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<YOUR_GITHUB_OCEAN_INTEGRATION_ID>",
"integrationProvider": "github-ocean",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "<GITHUB_ORG>",
"repo": "<GITHUB_REPO>",
"workflow": "provision-dev-env.yml",
"workflowInputs": {
"env_name": "{{ .outputs.trigger.name | gsub(\" \"; \"-\") | ascii_downcase }}",
"template": "{{ .outputs.trigger.catalog_env }}",
"ttl": "{{ .outputs.trigger.ttl }}",
"requested_by": "{{ .workflowRun.trigger.by.email }}"
},
"reportWorkflowStatus": true
}
}
},
{
"identifier": "upsert_new_env",
"title": "Add environment to catalog",
"icon": "DeployedAt",
"config": {
"type": "UPSERT_ENTITY",
"blueprintIdentifier": "developerEnv",
"mapping": {
"identifier": "{{ .outputs.trigger.name | gsub(\" \"; \"-\") | ascii_downcase }}",
"title": "{{ .outputs.trigger.name }}",
"team": ["{{ .outputs.trigger.team }}"],
"properties": {
"deploymentStatus": "Deployed",
"ttl": "{{ (.outputs.trigger.ttl | split(\" \") | .[0] | tonumber * 86400) as $toAdd | (now + $toAdd) | strftime(\"%Y-%m-%dT%H:%M:%S.000Z\") }}"
},
"relations": {
"catalog_env": "{{ .outputs.trigger.catalog_env }}",
"owner": "{{ .workflowRun.trigger.by.email }}"
}
}
}
}
],
"connections": [
{ "sourceIdentifier": "create_env_trigger", "targetIdentifier": "provision_env" },
{ "sourceIdentifier": "provision_env", "targetIdentifier": "upsert_new_env" }
]
}

Note the following details:

  • The trigger uses a CREATE_ENTITY context, so it also appears when a user clicks Create new entity on the developerEnv blueprint.
  • upsert_new_env sets relations.owner to the triggering user, so the requestor mirror property resolves automatically.
  • The ttl mapping converts the selected duration to seconds and adds it to now, then formats the result as ISO 8601 for the timer property. See set a dynamic expiration date for the JQ pattern.

Extend an environment's TTL

This is the only flow with an approval step. check_extend_tier routes the 7-day tier through extend_approval, while shorter extensions take the fallback connection straight to the dispatch node.

Extend TTL workflow JSON (Click to expand)
extend-developer-environment-ttl.json
{
"identifier": "extend_developer_environment_ttl",
"title": "Extend environment TTL",
"icon": "Clock",
"category": "Developer Environments",
"description": "Extend the TTL of an existing developer environment. Use when a user asks to renew or add more time. 7-day extensions need approval.",
"nodes": [
{
"identifier": "extend_ttl_trigger",
"title": "Extend environment TTL",
"icon": "Clock",
"config": {
"type": "SELF_SERVE_TRIGGER",
"contexts": [
{ "on": "ENTITY", "userInput": "environment" }
],
"userInputs": {
"properties": {
"environment": {
"type": "string",
"format": "entity",
"blueprint": "developerEnv",
"title": "Environment",
"description": "The developer environment whose TTL should be extended.",
"dataset": {
"combinator": "and",
"rules": [
{ "property": "deploymentStatus", "operator": "!=", "value": "Deleted" }
]
}
},
"ttl_extension": {
"type": "string",
"title": "Requested TTL extension",
"description": "The amount of time to add to the environment's current expiry. Accepted values: 1 day, 3 days, 7 days (Team Lead Approval).",
"enum": ["1 day", "3 days", "7 days (Team Lead Approval)"],
"enumColors": {
"1 day": "olive",
"3 days": "gold",
"7 days (Team Lead Approval)": "red"
}
}
},
"required": ["environment", "ttl_extension"],
"order": ["environment", "ttl_extension"]
},
"published": true
}
},
{
"identifier": "fetch_env",
"title": "Fetch current TTL",
"icon": "DeployedAt",
"config": {
"type": "WEBHOOK",
"url": "https://api.port.io/v1/blueprints/developerEnv/entities/search",
"method": "POST",
"body": {
"query": {
"combinator": "and",
"rules": [
{ "property": "$identifier", "operator": "=", "value": "{{ .outputs.trigger.environment }}" }
]
}
}
},
"variables": {
"entity": "{{ .result.response.data.entities[0] }}"
}
},
{
"identifier": "check_extend_tier",
"title": "Check requested extension",
"icon": "DefaultProperty",
"config": {
"type": "CONDITION",
"outlets": [
{
"identifier": "needs_approval",
"title": "Needs approval",
"expression": ".outputs.trigger.ttl_extension == \"7 days (Team Lead Approval)\""
}
]
}
},
{
"identifier": "extend_approval",
"title": "Team lead approval",
"icon": "DefaultProperty",
"config": {
"type": "INPUT",
"description": "{{ .outputs.trigger.environment }} requested a 7-day TTL extension. Current expiry: {{ .outputs.fetch_env.entity.properties.ttl }}.",
"userInputs": {
"properties": {},
"buttons": [
{ "identifier": "approve", "label": "Approve", "variant": "PRIMARY" },
{ "identifier": "decline", "label": "Decline", "variant": "DANGER" }
]
},
"outlets": [
{ "evaluationMethod": "button", "identifier": "approve", "title": "Approve", "numOfResponders": 1 },
{ "evaluationMethod": "button", "identifier": "decline", "title": "Decline", "numOfResponders": 1 }
],
"responders": {
"users": ["<TEAM_LEAD_EMAIL>"]
}
}
},
{
"identifier": "extend_env_lease",
"title": "Extend the environment lease",
"icon": "Github",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<YOUR_GITHUB_OCEAN_INTEGRATION_ID>",
"integrationProvider": "github-ocean",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "<GITHUB_ORG>",
"repo": "<GITHUB_REPO>",
"workflow": "extend-dev-env-ttl.yml",
"workflowInputs": {
"env_name": "{{ .outputs.trigger.environment }}",
"extension": "{{ .outputs.trigger.ttl_extension }}"
},
"reportWorkflowStatus": true
}
}
},
{
"identifier": "apply_ttl_extension",
"title": "Apply TTL extension",
"icon": "Clock",
"config": {
"type": "UPSERT_ENTITY",
"blueprintIdentifier": "developerEnv",
"mapping": {
"identifier": "{{ .outputs.trigger.environment }}",
"properties": {
"ttl": "{{ (.outputs.trigger.ttl_extension | split(\" \") | .[0] | tonumber * 86400) as $toAdd | ((.outputs.fetch_env.entity.properties.ttl | gsub(\"\\\\.[0-9]+Z$\"; \"Z\") | fromdateiso8601) + $toAdd) | strftime(\"%Y-%m-%dT%H:%M:%S.000Z\") }}"
}
}
}
}
],
"connections": [
{ "sourceIdentifier": "extend_ttl_trigger", "targetIdentifier": "fetch_env" },
{ "sourceIdentifier": "fetch_env", "targetIdentifier": "check_extend_tier" },
{ "sourceIdentifier": "check_extend_tier", "targetIdentifier": "extend_approval", "sourceOutletIdentifier": "needs_approval" },
{ "sourceIdentifier": "check_extend_tier", "targetIdentifier": "extend_env_lease", "fallback": true },
{ "sourceIdentifier": "extend_approval", "targetIdentifier": "extend_env_lease", "sourceOutletIdentifier": "approve" },
{ "sourceIdentifier": "extend_env_lease", "targetIdentifier": "apply_ttl_extension" }
]
}

Note the following details:

  • fetch_env reads the environment's current ttl through Port's entity search API, because the new expiry is calculated from the existing one rather than from now. Calls to Port's API from workflow nodes are authenticated automatically, so no token is needed.
  • Both the approved path and the fallback path converge on extend_env_lease, so the pipeline and the catalog update are defined once rather than duplicated per branch.
  • The INPUT node only continues on approve. A decline response ends the run, leaving the TTL untouched.
  • The environment picker filters out Deleted environments, so expired records cannot be extended back to life.

Delete a developer environment

Deletion is deliberately linear: confirm, tear down, mark deleted. The confirmation checkbox is enforced by a form validation on the trigger itself, so no branching node is needed on the canvas.

Delete workflow JSON (Click to expand)
delete-developer-environment.json
{
"identifier": "delete_developer_environment",
"title": "Delete a developer environment",
"icon": "Delete",
"category": "Developer Environments",
"description": "Tear down a developer environment and mark it deleted in the catalog. Use when a user asks to delete or decommission one.",
"nodes": [
{
"identifier": "delete_env_trigger",
"title": "Delete a developer environment",
"icon": "Delete",
"config": {
"type": "SELF_SERVE_TRIGGER",
"variant": "ALERT",
"contexts": [
{ "on": "ENTITY", "userInput": "environment" }
],
"userInputs": {
"properties": {
"environment": {
"type": "string",
"format": "entity",
"blueprint": "developerEnv",
"title": "Environment",
"description": "The developer environment to delete. Use its identifier from the catalog.",
"dataset": {
"combinator": "and",
"rules": [
{ "property": "deploymentStatus", "operator": "!=", "value": "Deleted" }
]
}
},
"confirm_deletion": {
"type": "boolean",
"title": "I understand this will permanently delete this environment",
"description": "This action is destructive and cannot be undone.",
"default": false
},
"deletion_reason": {
"type": "string",
"title": "Reason for deletion",
"description": "Helps with auditing and troubleshooting.",
"enum": ["No longer needed", "Tests complete", "Stuck or failed deployment", "Cost cleanup", "Recreating environment", "Other"]
}
},
"required": ["environment", "confirm_deletion"],
"order": ["environment", "confirm_deletion", "deletion_reason"],
"validations": [
{
"constraint": ".form.confirm_deletion",
"message": "You must confirm that this environment will be permanently deleted"
}
]
},
"published": true
}
},
{
"identifier": "teardown_env",
"title": "Tear down the environment",
"icon": "Github",
"config": {
"type": "INTEGRATION_ACTION",
"installationId": "<YOUR_GITHUB_OCEAN_INTEGRATION_ID>",
"integrationProvider": "github-ocean",
"integrationInvocationType": "dispatch_workflow",
"integrationActionExecutionProperties": {
"org": "<GITHUB_ORG>",
"repo": "<GITHUB_REPO>",
"workflow": "teardown-dev-env.yml",
"workflowInputs": {
"env_name": "{{ .outputs.trigger.environment }}",
"reason": "{{ .outputs.trigger.deletion_reason // \"Not specified\" }}"
},
"reportWorkflowStatus": true
}
}
},
{
"identifier": "mark_env_deleted",
"title": "Mark environment deleted",
"icon": "Delete",
"config": {
"type": "UPSERT_ENTITY",
"blueprintIdentifier": "developerEnv",
"mapping": {
"identifier": "{{ .outputs.trigger.environment }}",
"properties": {
"deploymentStatus": "Deleted",
"reason": "{{ .outputs.trigger.deletion_reason // \"Not specified\" }}"
}
}
}
}
],
"connections": [
{ "sourceIdentifier": "delete_env_trigger", "targetIdentifier": "teardown_env" },
{ "sourceIdentifier": "teardown_env", "targetIdentifier": "mark_env_deleted" }
]
}

Note the following details:

  • The trigger sets variant: "ALERT", so it renders with destructive red styling in the bolt menu.
  • validations constraints are evaluated against the form with the .form.<input> prefix, not .outputs.trigger. The rule catches an unchecked box, which required alone would not: a required boolean is satisfied by false.
  • mark_env_deleted sets deploymentStatus to Deleted instead of removing the entity, so the reason and the audit trail survive. Both the delete and extend pickers exclude Deleted environments, which also hides their triggers from those entities' bolt menus.
Confirmations apply to the form only

validations run when the self-service form is submitted. Runs started through the API or by an AI agent do not go through that form, so they can pass confirm_deletion: false and still proceed. Treat the confirmation as a safeguard against accidental clicks, not as a governance control. To gate every entry path, add a condition node after the trigger, or restrict who can run the workflow with permissions.

To remove the entity entirely rather than marking it Deleted, replace mark_env_deleted with a webhook node that calls DELETE https://api.port.io/v1/blueprints/developerEnv/entities/{{ .outputs.trigger.environment }}, as the manage clusters guide does. The dataset filters then become redundant.

Publish the workflows

Repeat the steps below for each of the three workflows.

  1. Go to the Workflows page in Port.

  2. Click on the + Workflow button.

  3. Fill out the Create new workflow form, then click Confirm.

  4. On the editor page, click the {...} button to open the JSON editor.

  5. Copy and paste one of the workflow JSONs above into the editor to replace the example workflow.

  6. Replace the placeholder values listed in the tip above.

  7. Click Apply changes to save the workflow.

GitHub integration action authentication

The GitHub integration action authenticates through your installed GitHub integration, so no token or secret is required in the node. For other providers such as GitLab, Bitbucket, or Azure DevOps, use a webhook node to hit the pipeline trigger URL, and pass credentials with {{ .secrets["<SECRET_NAME>"] }}.

4. Test the workflows

Trigger the workflows from whichever surface fits your setup:

  1. On the self-service page, find Create a developer environment, pick a catalog template, a TTL, and an owning team, then execute it. Follow the run from the workflow's runs view, and check the Actions tab of your repository to confirm the pipeline was dispatched.

  2. Open the bolt (⚡) menu on the new entity and run Extend environment TTL. Pick 1 day first to confirm it applies immediately, then try 7 days (Team Lead Approval) to see the run pause for approval.

  3. Open the bolt menu again and run Delete a developer environment. Try submitting with the checkbox unchecked to see the form block you, then confirm and submit. The entity stays in the catalog with its status set to Deleted, and it no longer appears in either picker.

Once triggered, the pieces come together:

  • Each workflow dispatches its GitHub Actions pipeline and waits for the run to finish.
  • Creation then adds a developerEnv entity with deploymentStatus set to Deployed and a ttl timer counting down to the computed expiry.
  • Extending pushes the ttl timer further out, immediately for 1 and 3 days, or after team lead approval for 7 days.
  • Deletion sets deploymentStatus to Deleted and records the reason.

Done! 🎉 You can now create, extend, and delete developer environments from Port, and any AI agent connected to Port can do the same.

Extend the workflows

  • Replace the placeholder pipelines with real provisioning logic, such as Terraform, Pulumi, or Helm.
  • Combine these workflows with a TIMER_PROPERTY_EXPIRED automation to notify an owner, or trigger the delete workflow automatically, once an environment's TTL runs out.
  • Add a cost guardrail that blocks creation when the selected template's cost_per_month exceeds a team's budget.
  • Relate developerEnv entities to the service blueprint so you can see which services each environment supports.

More relevant guides