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

Check out Port for yourself ➜ 

Scaffold a new service with Actions

Implement with AI

Send this guide to your coding agent.

Prerequisite: Install Port MCP

Open plan mode. Implement this Port guide in my org via MCP:

https://docs.port.io/guides/all/scaffold-a-new-service

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. Diff the guide's data model (blueprints, properties, relations, actions, agents, automations, integrations, secrets) against mine.
3. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates.
4. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out.
5. 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.
- 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, 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:
- Confirm the guide's expected output exists and runs in Port.
- Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.

This guide demonstrates how to set up a self-service action that allows developers to scaffold a new service.

A service in Port is a flexible concept, allowing you to represent a piece of software and its related components in a way that makes sense for you.

Use Port Workflows

Port Workflows are now available (open beta) and are recommended for building self-service experiences visually.
Check out the Scaffold a new service guide built using workflows.

The action we will create in this guide will:

  • Create a new Git repository.
  • Create a new service in Port, and relate it to the new repository, giving it its context.

Common use cases

  • Enable developers to independently spin up new microservices with boilerplate code.
  • Reduce friction for developers and prevent implementation differences by defining the logic of scaffolding a service and providing developers with an action they can simply execute.
  • Track created services using a wide array of visualizations.

Prerequisites

  • A Port account with permissions to create self-service actions.
  • The Git Integration that is relevant for you needs to be installed.
  • A repository in your Git provider in which you can create a workflow/pipeline.

Set up scaffolding action

Set up the action's frontend

  1. Head to the Self-service page of your portal.

  2. Click on the + Action button in the top-right corner (or in the middle if there are no actions yet):

    Add Action button in self-service page
  3. Fill the basic form with the Title and Description and select Create and Service for the Operation and Blueprint respectively.

    New action basic details form
  4. Click Next to proceed to the User Form tab, then click on + Input.

  5. Enter Service name as the Title, select Text for the Type, set Required to True, and click on the Create button.

    New user input form for service name

    If using Bitbucket, you will need to create these two additional inputs:

    Bitbucket requirements (Click to expand)
    Input NameTypeRequiredAdditional Information
    Bitbucket Workspace NameStringYesThe name of the workspace in which to create the new repository.
    Bitbucket Project KeyStringYesThe key of the Bitbucket project in which to create the new repository.
  6. Click Next to configure the Backend.

Define backend type

Now we'll define the backend of the action. Port supports multiple invocation types, depending on the Git provider you are using.

Fill out the form with your values:

  • Replace the Repository value with your value (this is where the workflow will reside and run).

  • Name the workflow port-create-service.yml.

  • Fill out your workflow details:

    GitHub Ocean workflow backend configuration form
  • Scroll down to the Configure the invocation payload section.
    This is where you can define which data will be sent to your backend each time the action is executed.

    For this example, we will send two details that our backend needs to know - the service name, and the id of the action run. Note that the service name will be the same as the repository name. Copy the following JSON snippet and paste it in the payload code box:

    {
    "port_context": {
    "runId": "{{ .run.id }}"
    },
    "service_name": "{{ .inputs.service_name }}"
    }

The last step is customizing the action's permissions. For simplicity's sake, we will use the default settings. For more information, see the permissions page. Click Create.

The action's frontend is now ready 🥳


Set up the action's backend

Now we want to write the logic that our action will trigger.

If the GitHub organization which will house your workflow is not the same as the one you'll create the new repository in, install GitHub ocean in the other organization as well.

  1. First, let's create the necessary token and secrets:

    • Go to your GitHub tokens page, create a personal access token (classic) with repo, admin:repo_hook and admin:org scope, and copy it (this token is needed to create a repo from our workflow).

      GitHub personal access token with repo scopes
      SAML SSO

      If your organization uses SAML SSO, you will need to authorize your token. Follow these instructions and then continue this guide.

    • Go to your Port application, click on your profile picture , then click Credentials. Copy your Client ID and Client secret.

  2. In the repository where your workflow will reside, create 3 new secrets under Settings->Secrets and variables->Actions:

    • ORG_ADMIN_TOKEN - the personal access token you created in the previous step.
    • PORT_CLIENT_ID - the client ID you copied from your Port app.
    • PORT_CLIENT_SECRET - the client secret you copied from your Port app.
    GitHub repository secrets with required tokens

  3. Now let's create the workflow file that contains our logic.
    First, ensure that you have a .github/workflows directory, then create a new file named port-create-service.yml and use the following snippet as its content (remember to change <YOUR-ORG-NAME> to your GitHub organization name):

    Personal (user) account note

    If you are scaffolding repositories under a personal GitHub account (user-owned repositories), the integration cannot rely on organization-level webhooks to discover newly created repositories immediately.

    To avoid githubRepository relation 404s when creating the service entity, set createPortEntity: true in your workflow under the step that uses port-labs/cookiecutter-gha@..., so the repository entity is created in Port as part of the scaffold.

    GitHub workflow (Click to expand)
    port-create-service.yml
    name: Scaffold a new service

    on:
    workflow_dispatch:
    inputs:
    service_name:
    required: true
    description: The name of the new service
    type: string
    description:
    required: false
    description: Description of the service
    type: string
    port_context:
    required: true
    description: Includes the action's run id
    type: string

    jobs:
    scaffold-service:
    runs-on: ubuntu-latest
    env:
    ORG_NAME: <YOUR-ORG-NAME>

    steps:
    - uses: actions/checkout@v6

    - name: Extract runId from port_context
    run: |
    echo "PORT_RUN_ID=$(echo '${{ inputs.port_context }}' | jq -r .runId)" >> $GITHUB_ENV

    - name: Create a log message
    uses: port-labs/port-github-action@v1
    with:
    clientId: ${{ secrets.PORT_CLIENT_ID }}
    clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    baseUrl: https://api.port.io
    operation: PATCH_RUN
    runId: ${{ env.PORT_RUN_ID }}
    logMessage: "Starting scaffolding of service and repository: ${{ inputs.service_name }}"

    - name: Create GitHub Repository
    uses: port-labs/cookiecutter-gha@v1.1.1
    with:
    portClientId: ${{ secrets.PORT_CLIENT_ID }}
    portClientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    token: ${{ secrets.ORG_ADMIN_TOKEN }}
    portRunId: ${{ fromJson(inputs.port_context).runId }}
    repositoryName: ${{ inputs.service_name }}
    portUserInputs: '{"cookiecutter_app_name": "${{ inputs.service_name }}" }'
    cookiecutterTemplate: https://github.com/lacion/cookiecutter-golang
    blueprintIdentifier: "githubRepository"
    organizationName: ${{ env.ORG_NAME }}
    createPortEntity: false


    - name: Create Service in Port with Repository Relation
    uses: port-labs/port-github-action@v1
    with:
    clientId: ${{ secrets.PORT_CLIENT_ID }}
    clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    baseUrl: https://api.port.io
    operation: UPSERT
    identifier: "${{ inputs.service_name }}_service"
    title: "${{ inputs.service_name }} Service"
    blueprint: "service"
    createMissingRelatedEntities: true
    relations: |
    { "github_repository": "${{ inputs.service_name }}" }

    - name: Create a log message
    uses: port-labs/port-github-action@v1
    with:
    clientId: ${{ secrets.PORT_CLIENT_ID }}
    clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    baseUrl: https://api.port.io
    operation: PATCH_RUN
    runId: ${{ env.PORT_RUN_ID }}
    logMessage: "Finished scaffolding of service and repository: ${{ inputs.service_name }}"

    - name: Report action status to Port
    uses: port-labs/port-github-action@v1
    with:
    clientId: ${{ secrets.PORT_CLIENT_ID }}
    clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    baseUrl: https://api.port.io
    operation: PATCH_RUN
    runId: ${{ env.PORT_RUN_ID }}
    status: "SUCCESS"
    summary: "Scaffolding of service and repository finished successfully"

    - name: Report failure status to Port
    if: failure()
    uses: port-labs/port-github-action@v1
    with:
    clientId: ${{ secrets.PORT_CLIENT_ID }}
    clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
    baseUrl: https://api.port.io
    operation: PATCH_RUN
    runId: ${{ env.PORT_RUN_ID }}
    status: "FAILURE"
    summary: "Failed to scaffold service and repository"

Cookiecutter template

The cookiecutter templates provided in the workflows are just examples, you can replace them with any other cookiecutter template you want to use, by changing the value of the relevant template variable in the workflow.

All done! The action is ready to be used 🚀

Execute the action

Head back to the Self-service page of your Port application:

Scaffold new service action card
  1. Click on Create to begin executing the action.

  2. Enter a name for your new repository.

    Repository name restrictions

    Some Git providers (for example, GitHub) do not allow spaces in repository names.
    We recommend using underscores or hyphens instead of spaces.

  3. For some of the available Git providers, additional inputs are required when executing the action.

    When executing the Bitbucket scaffolder, you will need to provide two additional inputs:

    • Bitbucket Workspace Name - the name of the workspace to create the new repository in.
      • Bitbucket Project Key - the key of the Bitbucket project to create the new repository in.
        • To find the Bitbucket project key, go to https://bitbucket.org/YOUR_BITBUCKET_WORKSPACE/workspace/projects/, find the desired project in the list, and copy the value seen in the Key column in the table.
  4. Click Execute. Click the button to view the action's progress.

  5. This page provides details about the action run. As you can see, the backend returned Success and the repo was successfully created (this can take a few moments):

    Action run details showing Success status and logs

    Logging action progress

    💡 Note the Log stream at the bottom, this can be used to report progress, results and errors. Click here to learn more.

  6. Head over to the service catalog and you will see a microservice entity created with the repository linked to it:

    Service catalog with new service and repository

Congratulations! You can now create services easily from Port.

Create pull requests for follow-up infrastructure changes

After a service is scaffolded, teams often need to add infrastructure code or configuration to the service repository. Use this optional flow to create a GitHub pull request from Port that appends a Terraform resource block to main.tf.

This example adds an Azure storage account resource, but you can replace the Terraform template with any resource your golden path needs.

Set up self-service action

Follow the steps below to create a self-service action that triggers a GitHub workflow.

  1. Go to the Self-service page of your portal.

  2. Click on the + New Action button.

  3. Click on the {...} Edit JSON button.

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

    Open GitHub PR action (Click to expand)
    Replace the variables
    • <GITHUB_ORG> - Your GitHub organization or user name.
    • <GITHUB_REPO> - The repository where the workflow file is stored.
    • <YOUR_GITHUB_OCEAN_INTEGRATION_ID> - Your GitHub Ocean integration installation ID.
    {
    "identifier": "open_github_pr",
    "title": "Open GitHub PR",
    "icon": "Microservice",
    "description": "Open a pull request after modifying a Terraform file.",
    "trigger": {
    "type": "self-service",
    "operation": "DAY-2",
    "userInputs": {
    "properties": {
    "storage_name": {
    "type": "string",
    "title": "Storage name"
    },
    "storage_location": {
    "type": "string",
    "title": "Storage location"
    }
    },
    "required": [
    "storage_name",
    "storage_location"
    ],
    "order": [
    "storage_name",
    "storage_location"
    ]
    },
    "blueprintIdentifier": "service"
    },
    "invocationMethod": {
    "type": "INTEGRATION_ACTION",
    "installationId": "<YOUR_GITHUB_OCEAN_INTEGRATION_ID>",
    "integrationActionType": "dispatch_workflow",
    "integrationActionExecutionProperties": {
    "org": "<GITHUB_ORG>",
    "repo": "<GITHUB_REPO>",
    "workflow": "create-github-pr.yml",
    "workflowInputs": {
    "storage_name": "{{ .inputs.storage_name }}",
    "storage_location": "{{ .inputs.storage_location }}",
    "repo_url": "{{ .entity.properties.url }}",
    "port_context": {
    "blueprint": "{{ .action.blueprint }}",
    "entity": "{{ .entity }}",
    "runId": "{{ .run.id }}",
    "trigger": "{{ .trigger }}"
    }
    },
    "reportWorkflowStatus": true
    }
    },
    "requiredApproval": false
    }
  5. Click Save to create the action.

Create Terraform templates

In the target service repository, create a templates/create-azure-storage.tf file:

Azure storage account template (Click to expand)
create-azure-storage.tf
resource "azurerm_storage_account" "storage_account" {
name = "{{ storage_name }}"
resource_group_name = "YourResourcesGroup"
location = "{{ storage_location }}"
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
}

Add a main.tf file in the root of the repository if it does not already exist:

Terraform provider configuration (Click to expand)
main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0.2"
}
}

required_version = ">= 1.1.0"
}

provider "azurerm" {
features {}
}

Create the GitHub workflow

Create .github/workflows/create-github-pr.yml in the workflows repository. Add the following repository secrets before running it:

  • PORT_CLIENT_ID - Your Port client ID.
  • PORT_CLIENT_SECRET - Your Port client secret.
  • GH_TOKEN - A classic personal access token with the repo scope.
Create GitHub PR workflow (Click to expand)
create-github-pr.yml
name: Create GitHub PR

on:
workflow_dispatch:
inputs:
storage_name:
required: true
type: string
storage_location:
required: true
type: string
repo_url:
required: true
type: string
port_context:
required: true
type: string

jobs:
create-pr:
runs-on: ubuntu-latest
steps:
- name: Inform starting
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{ fromJson(inputs.port_context).runId }}
logMessage: |
Creating a GitHub pull request for a new Terraform resource.

- name: Extract repository information
run: |
REPO_PATH=$(echo "${{ inputs.repo_url }}" | sed 's|https://github.com/||' | sed 's|git@github.com:||' | sed 's|.git||')
echo "REPO_PATH=$REPO_PATH" >> $GITHUB_ENV

- name: Checkout target repository
uses: actions/checkout@v6
with:
repository: ${{ env.REPO_PATH }}
token: ${{ secrets.GH_TOKEN }}
ref: main

- name: Make Terraform changes
run: |
sed "s/{{ storage_name }}/${{ inputs.storage_name }}/g; s/{{ storage_location }}/${{ inputs.storage_location }}/g" templates/create-azure-storage.tf >> main.tf

- name: Create branch and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "infra/new-resource-${{ inputs.storage_name }}"
git add main.tf
git commit -m "Add a new Terraform resource block"
git push origin "infra/new-resource-${{ inputs.storage_name }}"

- name: Create pull request
run: |
gh pr create --base main --head "infra/new-resource-${{ inputs.storage_name }}" \
--title "Add resource block ${{ inputs.storage_name }}" \
--body "This pull request adds a new Terraform resource block to the project."
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}

- name: Notify Port
uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{ fromJson(inputs.port_context).runId }}
status: SUCCESS
logMessage: |
Created GitHub pull request for new Terraform resource ${{ inputs.storage_name }}.

Deploy with Jenkins

Use Jenkins if your organization already manages GitOps automation through Jenkins pipelines.

First, create a second self-service action that invokes Jenkins:

Open GitHub PR with Jenkins action (Click to expand)
Replace the variables
  • YOUR_JENKINS_URL - The URL of your Jenkins server.
  • JOB_TOKEN - The token of the Jenkins job.
{
"identifier": "open_github_pr_with_jenkins",
"title": "Open GitHub PR with Jenkins",
"icon": "Microservice",
"description": "Open a pull request after modifying a Terraform file using Jenkins.",
"trigger": {
"type": "self-service",
"operation": "DAY-2",
"userInputs": {
"properties": {
"storage_name": {
"type": "string",
"title": "Storage name"
},
"storage_location": {
"type": "string",
"title": "Storage location"
}
},
"required": [
"storage_name",
"storage_location"
],
"order": [
"storage_name",
"storage_location"
]
},
"blueprintIdentifier": "service"
},
"invocationMethod": {
"type": "JENKINS",
"url": "http://YOUR_JENKINS_URL/generic-webhook-trigger/invoke?token=<JOB_TOKEN>",
"agent": false,
"body": {
"{{ spreadValue() }}": "{{ .inputs }}",
"port_context": {
"runId": "{{ .run.id }}",
"blueprint": "{{ .action.blueprint }}",
"entity": "{{ .entity }}"
}
}
},
"requiredApproval": false
}

Then configure Jenkins credentials:

  • port-credentials - A Jenkins Username with password credential where the username is PORT_CLIENT_ID and the password is PORT_CLIENT_SECRET.
  • WEBHOOK_TOKEN - The webhook token used by the Generic Webhook Trigger plugin.
  • GITHUB_TOKEN - A GitHub personal access token with permissions to create pull requests.

Create the GitHub personal access token from your GitHub tokens page, and grant it the repo and admin:org scopes.

GitHub personal access token with repository scopes

Before adding the Jenkinsfile, configure the Jenkins job:

Create a Jenkins pipeline that receives the Port payload, appends the Terraform template to main.tf, opens a pull request, and reports the run status back to Port:

Jenkins pipeline (Click to expand)
Jenkinsfile
import groovy.json.JsonSlurper

pipeline {
agent any

environment {
GITHUB_TOKEN = credentials("GITHUB_TOKEN")
NEW_BRANCH_PREFIX = "infra/new-resource"
NEW_BRANCH_NAME = "${NEW_BRANCH_PREFIX}-${STORAGE_NAME}"
TEMPLATE_FILE = "templates/create-azure-storage.tf"
PORT_ACCESS_TOKEN = ""
REPO = ""
}

triggers {
GenericTrigger(
genericVariables: [
[key: "STORAGE_NAME", value: "$.payload.properties.storage_name"],
[key: "STORAGE_LOCATION", value: "$.payload.properties.storage_location"],
[key: "REPO_URL", value: "$.payload.entity.properties.url"],
[key: "PORT_RUN_ID", value: "$.context.runId"]
],
causeString: "Triggered by Port",
allowSeveralTriggersPerBuild: true,
regexpFilterExpression: "",
regexpFilterText: "",
printContributedVariables: true,
printPostContent: true
)
}

stages {
stage("Checkout") {
steps {
script {
def path = REPO_URL.substring(REPO_URL.indexOf("/") + 1)
REPO = path.replace("/github.com/", "")
}

git branch: "main", credentialsId: "github", url: "git@github.com:${REPO}.git"
}
}

stage("Make changes") {
steps {
script {
sh """cat ${TEMPLATE_FILE} | sed "s/{{ storage_name }}/${STORAGE_NAME}/g; s/{{ storage_location }}/${STORAGE_LOCATION}/g" >> main.tf"""
}
}
}

stage("Create branch and commit") {
steps {
script {
sh "git checkout -b ${NEW_BRANCH_NAME}"
sh "git add main.tf"
sh "git commit -m 'Add a new Terraform resource block'"
sh "git push origin ${NEW_BRANCH_NAME}"
}
}
}

stage("Create pull request") {
steps {
script {
createPullRequestCurl(
REPO,
NEW_BRANCH_NAME,
"main",
"Add resource block ${STORAGE_NAME}",
"This pull request adds a new Terraform resource block to the project."
)
}
}
}

stage("Get access token") {
steps {
withCredentials([usernamePassword(
credentialsId: "port-credentials",
usernameVariable: "PORT_CLIENT_ID",
passwordVariable: "PORT_CLIENT_SECRET"
)]) {
script {
def result = sh(returnStdout: true, script: """
accessTokenPayload=\$(curl -X POST \
-H "Content-Type: application/json" \
-d '{"clientId": "${PORT_CLIENT_ID}", "clientSecret": "${PORT_CLIENT_SECRET}"}' \
-s "https://api.port.io/v1/auth/access_token")
echo \$accessTokenPayload
""")

def jsonSlurper = new JsonSlurper()
def payloadJson = jsonSlurper.parseText(result.trim())
PORT_ACCESS_TOKEN = payloadJson.accessToken
}
}
}
}

stage("Notify Port") {
steps {
script {
sh """
curl -X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
-d '{"status":"SUCCESS", "message": {"run_status": "Jenkins run completed successfully"}}' \
"https://api.port.io/v1/actions/runs/${PORT_RUN_ID}"
"""
}
}
}
}

post {
failure {
script {
sh """
curl -X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
-d '{"status":"FAILURE", "message": {"run_status": "Failed to create Terraform resource ${STORAGE_NAME}"}}' \
"https://api.port.io/v1/actions/runs/${PORT_RUN_ID}"
"""
}
}

always {
cleanWs(cleanWhenNotBuilt: false, deleteDirs: true, disableDeferredWipeout: false, notFailBuild: true)
}
}
}

def createPullRequestCurl(repo, headBranch, baseBranch, title, body) {
curlCommand = "curl -X POST https://api.github.com/repos/$repo/pulls -H 'Authorization: Bearer ${GITHUB_TOKEN}' -d '{ \"head\": \"$headBranch\", \"base\": \"$baseBranch\", \"title\": \"$title\", \"body\": \"$body\", \"draft\": false }'"

try {
response = sh(script: curlCommand)

if (response.contains("201 Created")) {
println "Pull request created successfully"
} else {
println "Failed to create pull request"
println response
}
} catch (Exception e) {
println "Error occurred during CURL request: ${e.getMessage()}"
}
}

Test the pull request flow

  1. Go to the Self-service page of your portal.

  2. Click the Open GitHub PR action, or Open GitHub PR with Jenkins if you use the Jenkins path.

  3. Enter a name for the Azure storage account and a location.

  4. Select the relevant service and click Execute.

  5. Open the action run details and verify that the run succeeded.

  6. Check the GitHub repository and confirm that the pull request was created with the Terraform changes.

To trigger resource deployment after the pull request is merged, use the Jenkins Terraform Azure pipeline example.

Possible daily routine integrations

  • Send a Slack message in the R&D channel to let everyone know that a new service was created.
  • Send a weekly or monthly report for managers showing all the new services created in this timeframe and their owners.

More relevant guides