Create foundational Engineering Intelligence data model
If you signed up for Port on or after May 1, 2026, the Engineering Intelligence data model is already set up in your portal. You can follow this guide to customize it to fit your organization's needs. Learn more about Port's organization hierarchy.
This guide walks you through setting up the foundational data model that powers Port's Engineering Intelligence capabilities.
By the end, you'll have a solid foundation of organizations, teams, services, and repositories all connected through relations, giving you the flexibility to power Port's Engineering Intelligence with visibility at any level from individual services to teams and across the entire organization.
The data model follows a modular architecture: a shared core layer (organization, team, service) plus blueprints created by your integrations. This guide covers GitHub, GitLab, and Azure DevOps, but the core layer is provider-agnostic and stays consistent regardless of which SCM tools you use.
A strong foundational data model is the backbone of engineering intelligence. Well-defined relations between organizations, teams, services, and the tools that support them make it easy to aggregate metrics at the right level, enforce standards through scorecards, and automate workflows across your engineering organization.
How it works
When you connect an SCM integration (GitHub, GitLab, or Azure DevOps), Port automatically generates blueprints for repositories, pull/merge requests, pipelines/workflows, and related objects, then begins ingesting live data into your catalog. Engineering Intelligence properties (DORA metrics, PR throughput, PR cycle time, change failure rate) are calculated directly on those integration entities.
To surface metrics at the service, team, and organization level, create a service and link it to a repository and team. Port then aggregates metrics up the hierarchy from service to team to group to organization, recalculating every 15 minutes.
Common use cases
- Track DORA metrics across your organization, teams, and services.
- Enable delivery performance metrics like PR/MR cycle time, throughput, staleness, lead time, and more.
- Define scorecards at different levels (organization, team, service) and combine them with workflows and automations to enforce engineering standards.
- Create a unified view of engineering assets across multiple integrations, from SCM and CI/CD to incident management and cloud infrastructure.
- Power AI agents with structured engineering data for intelligent automation.
Data model overview
The data model has two layers: a provider-agnostic core (Organization, Team, Service) that stays consistent regardless of which tools you use, and integration-specific blueprints created automatically when you connect your SCM and incident management tools.
Each SCM integration ships with blueprints that power different Engineering Intelligence use cases. The tables below show example blueprints used across the EI guides; your integration may ship with additional ones (releases, environments, etc.) not listed here.
- GitHub (Ocean)
- GitLab
- Azure DevOps
| Example blueprint | Example use case |
|---|---|
GitHub Repository (githubRepository) | Production Readiness scorecard |
GitHub Pull Request (githubPullRequest) | Delivery Performance metrics |
GitHub Workflow Run (githubWorkflowRun) | Measure pipeline reliability |
| Example blueprint | Example use case |
|---|---|
GitLab Repository (gitlabRepository) | Production Readiness scorecard |
GitLab Merge Request (gitlabMergeRequest) | Delivery Performance metrics |
GitLab Job (gitlabJob) | Measure pipeline reliability |
| Example blueprint | Example use case |
|---|---|
Azure Repository (azureDevopsRepository) | Production Readiness scorecard |
Azure Pull Request (azureDevopsPullRequest) | Delivery Performance metrics |
Azure Devops Build (azureDevopsBuild) | Measure pipeline reliability |
Example team hierarchy
Below is one example of how to structure team levels, each rolling up metrics to the next. Your organization may use fewer levels or different names:
| Level | Blueprint | Type | Members | Description | Example | Example use case |
|---|---|---|---|---|---|---|
| 1 | Service | - | - | Metrics calculated per service. | payments-api | Track delivery performance for a specific service. |
| 2 | Team | team | Users | Metrics roll up from services to teams. | payments-experience | Compare cycle time, throughput, and MTTR across teams. |
| 3 | Team | group | Teams (type: team) | Rollups for cross-team comparison at scale. | digital-banking | Benchmark DORA and delivery performance across a business unit or product. |
| 4 | Organization | - | Groups | Company-wide rollup - the single number for execs and QBR-level reporting. | Acme Corp | Measure engineering health across the entire organization. |
Add a parent_team to model deeper structures. For example: service → team → product → business unit → organization.
Prerequisites
To follow this guide, you need:
- A Port account with the onboarding process completed.
- At least one SCM integration installed: GitHub (Ocean), GitLab, or Azure DevOps.
Verify your SCM integration
Before touching any blueprints, confirm your SCM integration is installed and has already created its default blueprints.
- Go to the Data Sources page of your portal.
- Confirm your SCM integration appears and its status is active.
- Go to the Builder page and confirm the following blueprints exist for your provider:
- GitHub (Ocean)
- GitLab
- Azure DevOps
| Blueprint | Identifier | Required for |
|---|---|---|
| GitHub Organization | githubOrganization | Core data model |
| GitHub Team | githubTeam | Core data model |
| GitHub User | githubUser | Core data model |
| GitHub Repository | githubRepository | Core data model |
| GitHub Pull Request | githubPullRequest | Measure PR delivery metrics, Delivery performance scorecard, DORA metrics |
| GitHub Workflow | githubWorkflow | Measure pipeline reliability |
| GitHub Workflow Run | githubWorkflowRun | Measure pipeline reliability, Pipeline reliability scorecard |
| Blueprint | Identifier | Required for |
|---|---|---|
| GitLab Namespace | gitlabOrganization | Core data model |
| GitLab Group | gitlabGroup | Core data model |
| GitLab Member | gitlabMember | Core data model |
| GitLab Repository | gitlabRepository | Core data model |
| GitLab Merge Request | gitlabMergeRequest | Measure PR delivery metrics, Delivery performance scorecard, DORA metrics |
| GitLab Pipeline | gitlabPipeline | Measure pipeline reliability |
| GitLab Job | gitlabJob | Measure pipeline reliability, Pipeline reliability scorecard |
| Blueprint | Identifier | Required for |
|---|---|---|
| Azure Project | azureDevopsProject | Core data model |
| Azure Devops Team | azureDevopsTeam | Core data model |
| Azure Devops User | azureDevopsUser | Core data model |
| Azure Repository | azureDevopsRepository | Core data model |
| Azure Pull Request | azureDevopsPullRequest | Measure PR delivery metrics, Delivery performance scorecard, DORA metrics |
| Azure Devops Pipeline Run | azureDevopsPipelineRun | Measure pipeline reliability |
| Azure Devops Build | azureDevopsBuild | Measure pipeline reliability, Pipeline reliability scorecard |
If any blueprint above is absent, trigger a manual resync of your integration - the integration recreates its default blueprints on resync. If a blueprint still doesn't appear, refer to your integration's documentation: GitHub, GitLab, Azure DevOps. If you don't need the use case listed in the Required for column, you can safely skip that blueprint.
Set up data model
You'll set up blueprints in a specific order to satisfy relation dependencies. For each blueprint below, you will either create it (if it doesn't exist yet) or edit the existing one to add the required properties and relations.
Set up the blueprints in the order listed - each may depend on the previous one.
Core blueprints
These blueprints form the integration-agnostic core of the data model.
Set up the Organization blueprint
-
Go to the Builder page of your portal.
-
If an Organization blueprint already exists, click on it. Otherwise, click + Blueprint to create a new one.
-
Click on the
{...}button in the top right corner, and choose Edit JSON. -
Merge the following JSON definition with any existing content and click Save:
Merge, don't replaceIf you re-run this guide after following downstream Engineering Intelligence guides (DORA, delivery performance, pipeline reliability), the
organizationblueprint may already hold calculation and aggregation properties those guides added. Replacing the JSON wholesale would wipe them. Merge the baseline below into whatever is already there.Organization blueprint (Click to expand)
{"identifier": "organization","title": "Organization","icon": "Organization","description": "A logical organization grouping teams and services","schema": {"properties": {},"required": []}}
Extend the Team blueprint
The _team blueprint is a built-in Port system blueprint. You need to extend it with additional properties and relations for team hierarchy.
-
Go to the Builder page of your portal.
-
Find the Team blueprint and click on it.
-
Click on the
{...}button in the top right corner, and choose Edit JSON. -
Merge the following into the existing definition and click Save:
Merge, don't replaceThe
_teamblueprint is a Port system blueprint that may already have properties and relations added by other guides or your own customizations. Replacing the JSON wholesale would wipe them. Merge only theschema,relations, andmirrorPropertiesfields shown below into whatever is already there.Team blueprint extensions (Click to expand)
{"identifier": "_team","schema": {"properties": {"description": { "title": "Description", "type": "string" },"type": {"title": "Type","type": "string","description": "Hierarchy level - team (leaf) or group (intermediate)","enum": ["team", "group"],"enumColors": {"team": "blue","group": "turquoise"},"default": "team"}}},"relations": {"parent_team": {"target": "_team","title": "Parent Team","many": false,"required": false},"organization": {"target": "organization","title": "Organization","many": false,"required": false}},"mirrorProperties": {"parent_team_name": {"title": "Parent Team","path": "parent_team.$title"}}}
Set up the Service blueprint
The service blueprint represents an integration-agnostic service.
Team ownership is handled through Port's built-in $team field (declared via "ownership": { "type": "Direct" }), so the service blueprint itself does not need a team relation.
Most repository-level data (URL, README, language, last activity) is exposed via mirror properties from the linked repository blueprint, so the same fields stay in sync without duplication.
The service blueprint defines relations to githubRepository, gitlabRepository, and azureDevopsRepository. Each repository blueprint is created automatically by its corresponding SCM integration (see Prerequisites). Only include the relations and mirror properties for SCMs you have installed - including a relation that targets a blueprint that doesn't exist yet will cause a "target blueprint not found" error on save.
-
Go to the Builder page of your portal.
-
If a Service blueprint already exists, click on it. Otherwise, click + Blueprint to create a new one.
-
Click on the
{...}button in the top right corner, and choose Edit JSON. -
Merge the following JSON definition with any existing content and click Save:
Merge, don't replaceIf you re-run this guide after following downstream Engineering Intelligence guides, the
serviceblueprint may already hold calculation, aggregation, and mirror properties those guides added (e.g., DORA tier calcs, PR/MR aggregations, pipeline failure rates). Replacing the JSON wholesale would wipe them. Merge the baseline below into whatever is already there.Service blueprint (Click to expand)
{"identifier": "service","title": "Service","icon": "Microservice","ownership": { "type": "Direct" },"schema": {"properties": {"criticality": {"title": "Criticality","icon": "Alert","description": "Service criticality level","type": "string","enum": ["low", "medium", "high", "critical"],"enumColors": {"low": "turquoise","medium": "yellow","high": "orange","critical": "red"}}},"required": []},"relations": {"github_repository": {"target": "githubRepository","title": "GitHub repository","many": false,"required": false},"gitlab_repository": {"target": "gitlabRepository","title": "GitLab repository","many": false,"required": false},"azureDevopsRepository": {"target": "azureDevopsRepository","title": "Azure repository","many": false,"required": false}},"mirrorProperties": {"github_repository_id": { "title": "GitHub Repository identifier", "path": "github_repository.$identifier" },"github_url": { "title": "GitHub URL", "path": "github_repository.url" },"github_readme": { "title": "GitHub README", "path": "github_repository.readme" },"github_codeowners": { "title": "GitHub Code Owners", "path": "github_repository.codeowners" },"github_description": { "title": "GitHub Description", "path": "github_repository.description" },"github_language": { "title": "GitHub Language", "path": "github_repository.language" },"github_default_branch": { "title": "GitHub Default Branch", "path": "github_repository.defaultBranch" },"github_last_push": { "title": "GitHub Last Push", "path": "github_repository.last_push" },"github_visibility": { "title": "GitHub Visibility", "path": "github_repository.visibility" },"github_gitignore": { "title": "GitHub .gitignore", "path": "github_repository.gitignore" },"github_pr_template": { "title": "GitHub PR Template", "path": "github_repository.pr_template" },"gitlab_repository_identifier": { "title": "GitLab Repository identifier", "path": "gitlab_repository.$identifier" },"gitlab_url": { "title": "GitLab URL", "path": "gitlab_repository.url" },"gitlab_readme": { "title": "GitLab README", "path": "gitlab_repository.readme" },"gitlab_codeowners": { "title": "GitLab Code Owners", "path": "gitlab_repository.codeowners" },"gitlab_description": { "title": "GitLab Description", "path": "gitlab_repository.description" },"gitlab_language": { "title": "GitLab Language", "path": "gitlab_repository.language" },"gitlab_default_branch": { "title": "GitLab Default Branch", "path": "gitlab_repository.defaultBranch" },"gitlab_last_activity": { "title": "GitLab Last Activity", "path": "gitlab_repository.last_activity" },"ado_repository_id": { "title": "Azure Repository identifier", "path": "azureDevopsRepository.$identifier" },"ado_url": { "title": "Azure URL", "path": "azureDevopsRepository.url" },"ado_readme": { "title": "Azure README", "path": "azureDevopsRepository.readme" },"ado_codeowners": { "title": "Azure Code Owners", "path": "azureDevopsRepository.codeowners" },"ado_last_activity": { "title": "Azure Last Activity", "path": "azureDevopsRepository.last_activity" },"ado_minimumApproverCount": { "title": "Minimum Approver Count", "path": "azureDevopsRepository.minimumApproverCount" }}}
If you only use one SCM, keep only that provider's relation and mirror properties. For example, a GitHub-only org keeps github_repository and the github_* mirror properties, GitLab-only or Azure DevOps-only setups follow the same pattern.
team relation on service?Port's service blueprint uses the built-in team ownership model ("ownership": { "type": "Direct" }). Each entity has a $team system field that can hold one or more teams. Aggregations on the _team blueprint use this field to roll up metrics from owned services, so you do not need a separate team relation.
SCM integration blueprints
This section configures the SCM blueprints that link the foundation layer to your repositories, teams, and users. The exact blueprints depend on which SCM(s) you have installed - select your provider below.
For each blueprint:
- Go to the Builder page.
- Find the blueprint and click on it.
- Click on the
{...}button in the top right corner, and choose Edit JSON. - Merge the JSON definition below into the existing blueprint and click Save.
When editing blueprints created by the integration, merge the JSON below with the existing definition - don't replace it entirely. The integration may have added properties (such as metric aggregations, calculations, or scorecards) that you want to keep. Focus on confirming the relations shown below.
- GitHub (Ocean)
- GitLab
- Azure DevOps
The GitHub integration ships with these foundation blueprints: githubOrganization, githubTeam, githubUser, and githubRepository. The remaining blueprints (githubPullRequest, githubWorkflow, githubWorkflowRun) are configured by the use case guides that use them.
You only need to verify the relations below point to the right targets:
githubOrganization.organization→ coreorganizationblueprintgithubTeam.organization→githubOrganizationgithubUser.team→githubTeam(many)githubRepository.githubTeams→githubTeam(many)
GitHub Organization blueprint (Click to expand)
{
"identifier": "githubOrganization",
"title": "GitHub Organization",
"icon": "Github",
"description": "Top-level GitHub organization - container for repositories and teams",
"schema": {
"properties": {
"login": { "title": "Organization Login", "type": "string" },
"url": { "title": "URL", "type": "string", "format": "url" },
"description": { "title": "Description", "type": "string" },
"avatarUrl": { "title": "Avatar URL", "type": "string" },
"publicRepos": { "title": "Public Repositories", "type": "number" },
"createdAt": { "title": "Created At", "type": "string", "format": "date-time" }
}
},
"relations": {
"organization": {
"target": "organization",
"title": "Organization",
"many": false,
"required": false
}
}
}
GitHub Team blueprint (Click to expand)
{
"identifier": "githubTeam",
"title": "GitHub Team",
"icon": "Github",
"schema": {
"properties": {
"slug": { "title": "Slug", "type": "string" },
"description": { "title": "Description", "type": "string" },
"link": { "title": "Link", "type": "string", "format": "url" }
}
},
"relations": {
"organization": {
"target": "githubOrganization",
"title": "Organization",
"many": false,
"required": false
}
}
}
GitHub User blueprint (Click to expand)
{
"identifier": "githubUser",
"title": "GitHub User",
"icon": "Github",
"schema": {
"properties": {
"email": { "title": "Email", "type": "string" },
"login": { "title": "Login", "type": "string" }
}
},
"relations": {
"team": {
"target": "githubTeam",
"title": "GitHub Team",
"many": true,
"required": false
}
}
}
GitHub Repository blueprint (Click to expand)
The githubRepository blueprint stays largely as the integration creates it. The link to service is on the service side (via the github_repository relation you defined above). The integration adds metric aggregations and calculations for PR throughput, cycle time, and workflow failure rates.
{
"identifier": "githubRepository",
"title": "GitHub Repository",
"icon": "Github",
"ownership": { "type": "Direct" },
"schema": {
"properties": {
"url": { "title": "Repository URL", "type": "string", "format": "url" },
"readme": { "title": "README", "type": "string", "format": "markdown" },
"codeowners": { "title": "CODEOWNERS", "type": "string", "format": "markdown" },
"defaultBranch": { "title": "Default branch", "type": "string" },
"description": { "title": "Description", "type": "string" },
"visibility": { "title": "Visibility", "type": "string" },
"language": { "title": "Language", "type": "string" },
"last_push": { "title": "Last Repository Push", "type": "string", "format": "date-time" },
"gitignore": { "title": "Git Ignore", "type": "string" },
"pr_template": { "title": "PR Template", "type": "string" }
}
},
"relations": {
"githubTeams": {
"target": "githubTeam",
"title": "GitHub Teams",
"many": true,
"required": false
}
}
}
The GitLab integration ships with these foundation blueprints: gitlabOrganization (namespace), gitlabGroup, gitlabMember, and gitlabRepository. The remaining blueprints (gitlabMergeRequest, gitlabPipeline, gitlabJob) are configured by the use case guides that use them.
You only need to verify the relations below point to the right targets:
gitlabOrganization.organization→ coreorganizationblueprintgitlabGroup.organization→gitlabOrganization,gitlabGroup.gitlabMembers→gitlabMember
GitLab Namespace blueprint (Click to expand)
{
"identifier": "gitlabOrganization",
"title": "GitLab Namespace",
"icon": "GitLab",
"description": "Top-level GitLab namespace - container for groups and projects",
"schema": {
"properties": {
"url": { "title": "URL", "type": "string", "format": "url" },
"description": { "title": "Description", "type": "string" }
}
},
"relations": {
"organization": {
"target": "organization",
"title": "Organization",
"many": false,
"required": false
}
}
}
GitLab Group blueprint (Click to expand)
{
"identifier": "gitlabGroup",
"title": "GitLab Group",
"icon": "GitLab",
"schema": {
"properties": {
"visibility": {
"title": "Visibility",
"type": "string",
"enum": ["public", "internal", "private"],
"enumColors": { "public": "red", "internal": "yellow", "private": "green" }
},
"url": { "title": "URL", "type": "string", "format": "url" },
"description": { "title": "Description", "type": "string" }
}
},
"relations": {
"organization": {
"target": "gitlabOrganization",
"title": "Namespace",
"many": false,
"required": false
},
"gitlabMembers": {
"target": "gitlabMember",
"title": "Members",
"many": true,
"required": false
}
}
}
GitLab Member blueprint (Click to expand)
{
"identifier": "gitlabMember",
"title": "GitLab Member",
"icon": "GitLab",
"schema": {
"properties": {
"email": { "title": "Email", "type": "string", "format": "user" },
"link": { "title": "Link", "type": "string", "format": "url" },
"state": { "title": "State", "type": "string" },
"locked": { "title": "Locked", "type": "string" }
}
},
"relations": {}
}
GitLab Repository blueprint (Click to expand)
The gitlabRepository blueprint stays as the integration creates it. The link to service is on the service side (via the gitlab_repository relation you defined above).
{
"identifier": "gitlabRepository",
"title": "GitLab Repository",
"icon": "GitLab",
"schema": {
"properties": {
"url": { "title": "URL", "type": "string", "format": "url" },
"readme": { "title": "README", "type": "string", "format": "markdown" },
"codeowners": { "title": "CODEOWNERS", "type": "string", "format": "markdown" },
"description": { "title": "Description", "type": "string" },
"language": { "title": "Language", "type": "string" },
"namespace": { "title": "Namespace", "type": "string" },
"fullPath": { "title": "Full Path", "type": "string" },
"defaultBranch": { "title": "Default Branch", "type": "string" },
"last_activity": { "title": "Last Activity", "type": "string", "format": "date-time" }
}
},
"relations": {}
}
The Azure DevOps integration ships with these foundation blueprints: azureDevopsProject, azureDevopsTeam, azureDevopsUser, and azureDevopsRepository. The remaining blueprints (azureDevopsPullRequest, azureDevopsBuild, azureDevopsPipelineRun) are configured by the use case guides that use them.
You only need to verify the relations below point to the right targets:
azureDevopsProject.organization→ coreorganizationblueprintazureDevopsTeam.project→azureDevopsProject,azureDevopsTeam.members→azureDevopsUser(many)azureDevopsRepository.project→azureDevopsProject
Azure Project blueprint (Click to expand)
{
"identifier": "azureDevopsProject",
"title": "Azure Project",
"icon": "AzureDevops",
"description": "Azure DevOps project - container for repositories, teams, and pipelines",
"schema": {
"properties": {
"state": { "title": "State", "type": "string" },
"revision": { "title": "Revision", "type": "string" },
"visibility": { "title": "Visibility", "type": "string" },
"defaultTeam": { "title": "Default Team", "type": "string", "icon": "Team" },
"link": { "title": "Link", "type": "string", "format": "url" }
}
},
"relations": {
"organization": {
"target": "organization",
"title": "Organization",
"many": false,
"required": false
}
}
}
Azure DevOps Team blueprint (Click to expand)
{
"identifier": "azureDevopsTeam",
"title": "Azure Devops Team",
"icon": "AzureDevops",
"schema": {
"properties": {
"url": { "title": "URL", "type": "string", "format": "url" },
"description": { "title": "Description", "type": "string" }
}
},
"relations": {
"project": {
"target": "azureDevopsProject",
"title": "Project",
"many": false,
"required": true
},
"members": {
"target": "azureDevopsUser",
"title": "Members",
"many": true,
"required": false
}
}
}
Azure DevOps User blueprint (Click to expand)
{
"identifier": "azureDevopsUser",
"title": "Azure Devops User",
"icon": "AzureDevops",
"schema": {
"properties": {
"url": { "title": "URL", "type": "string", "format": "url" },
"email": { "title": "Email", "type": "string" }
}
},
"relations": {}
}
Azure Repository blueprint (Click to expand)
The azureDevopsRepository blueprint stays as the integration creates it. The link to service is on the service side (via the azureDevopsRepository relation you defined above).
{
"identifier": "azureDevopsRepository",
"title": "Azure Repository",
"icon": "AzureDevops",
"schema": {
"properties": {
"url": { "title": "URL", "type": "string", "format": "url" },
"readme": { "title": "README", "type": "string", "format": "markdown" },
"codeowners": { "title": "CODEOWNERS", "type": "string", "format": "markdown" },
"id": { "title": "ID", "type": "string" },
"last_activity": { "title": "Last Activity", "type": "string", "format": "date-time" },
"minimumApproverCount": { "title": "Minimum Approver Count", "type": "number" },
"workItemLinking": { "title": "Work Item Linking", "type": "boolean", "default": false }
}
},
"relations": {
"project": {
"target": "azureDevopsProject",
"title": "Project",
"many": false,
"required": true
}
}
}
Create the default organization, group, and team
Before you configure the integration mapping, create one Organization, one Group, and one Team entity to act as the default owners for the data the integration will ingest. You can rename or add more later, but starting with a single set of defaults keeps the setup straightforward.
- Navigate to the Software Catalog.
- Open the Organization page and click + Organization to create an entity with:
- Identifier:
default-org - Title:
default-org
- Identifier:
- Open the Team page and click + Team to create a
groupentity with:- Identifier:
default-group - Title:
default-group - Type:
group - Organization:
default-org
- Identifier:
- Click + Team again to create a
teamentity with:- Identifier:
default-team - Title:
default-team - Type:
team - Parent Team:
default-group - Organization: leave empty - the team's organization is reachable via
default-team.parent_team.organization. Setting it directly is optional and has no effect on team-level aggregations, which work via Port's$teamownership rather than theorganizationrelation.
- Identifier:
The integration mappings in the next step reference "default-org" for the relation that ties each SCM's top-level container (GitHub Organization, GitLab Namespace, or Azure Project) to the core organization blueprint. If you choose a different identifier for the default organization entity, update the mappings accordingly.
Configure integration mapping
After setting up the blueprints, configure your SCM integration's mapping so data flows into the correct blueprints and relations. Without this step, the blueprints exist but remain empty.
The mappings below are the complete EI configuration for each provider. They cover everything needed for this guide and all downstream EI guides (delivery performance, DORA, pipeline reliability): organizations, teams, users, repositories, pull/merge requests, and workflows/pipelines/builds.
If your integration already has a custom mapping, merge these blocks into it rather than replacing the entire config. Read the existing mapping first so you don't overwrite customizations.
- Go to the Data Sources page of your portal.
- Find your SCM integration and click on it.
- Read the existing mapping config before making changes.
- Apply the complete EI mapping below for your provider:
- GitHub (Ocean)
- GitLab
- Azure DevOps
GitHub integration mapping - complete EI configuration (Click to expand)
resources:
- kind: organization
selector:
query: "true"
port:
entity:
mappings:
identifier: .login
title: .login
blueprint: '"githubOrganization"'
properties:
login: .login
id: .id
nodeId: .node_id
url: .url
avatarUrl: .avatar_url
description: 'if .description then .description else "" end'
createdAt: .created_at
updatedAt: .updated_at
publicRepos: .public_repos
relations:
organization: '"default-org"'
- kind: team
selector:
query: "true"
members: true
port:
entity:
mappings:
identifier: .databaseId | tostring
title: .name
blueprint: '"githubTeam"'
properties:
slug: .slug
description: .description
link: .url
notification_setting: .notificationSetting
relations:
organization: '.url | capture("orgs/(?<org>[^/]+)") | .org'
- kind: user
selector:
query: "true"
port:
entity:
mappings:
identifier: .login
title: .login
blueprint: '"githubUser"'
properties:
email: .email
login: .login
- kind: team
selector:
query: "true"
members: true
port:
itemsToParse: .members.nodes
entity:
mappings:
identifier: .item.login
title: .item.login
blueprint: '"githubUser"'
relations:
team: '[.parent.id | tostring]'
- kind: repository
selector:
query: "true"
include: ["teams"]
includedFiles:
- README.md
- CODEOWNERS
- .gitignore
- .github/pull_request_template.md
port:
entity:
mappings:
identifier: .name
title: .name
blueprint: '"githubRepository"'
properties:
url: .html_url
readme: '.__includedFiles["README.md"]'
codeowners: '.__includedFiles["CODEOWNERS"]'
description: 'if .description then .description else "" end'
defaultBranch: .default_branch
language: 'if .language then .language else "" end'
last_push: .pushed_at
gitignore: '.__includedFiles[".gitignore"]'
pr_template: '.__includedFiles[".github/pull_request_template.md"]'
visibility: 'if .private then "private" else "public" end'
relations:
githubTeams: '[.__teams[].id | tostring]'
- kind: pull-request
selector:
query: "true"
maxResults: 300
since: 90
states:
- open
- closed
port:
entity:
mappings:
identifier: '.head.repo.name + "-pr-" + (.number | tostring)'
title: .title
blueprint: '"githubPullRequest"'
properties:
branch: .head.ref
closedAt: .closed_at
createdAt: .created_at
cycle_time_hours: 'if .merged_at != null and .created_at != null then ((.merged_at | fromdateiso8601) - (.created_at | fromdateiso8601)) / 3600 else null end'
has_assignees: '(.assignees | length) != 0'
has_reviewers: '(.requested_reviewers | length) != 0'
link: .html_url
mergedAt: .merged_at
prNumber: .number
status: 'if .merged_at != null then "merged" elif .state == "closed" then "closed" else "open" end'
relations:
git_hub_assignees: '[.assignees[].login]'
git_hub_creator: .user.login
git_hub_reviewers: '[.requested_reviewers[].login]'
repository: .head.repo.name
creator:
combinator: '"and"'
rules:
- property: '"git_hub_username"'
operator: '"="'
value: .user.login
reviewers:
combinator: '"and"'
rules:
- property: '"git_hub_username"'
operator: '"in"'
value: '[.requested_reviewers[]?.login // empty]'
assignees:
combinator: '"and"'
rules:
- property: '"git_hub_username"'
operator: '"in"'
value: '[.assignees[]?.login // empty]'
service:
combinator: '"and"'
rules:
- property: '"github_repository_id"'
operator: '"="'
value: .head.repo.name
- kind: workflow
selector:
query: "true"
port:
entity:
mappings:
identifier: '(.url | capture("repos/(?<repo>[^/]+/[^/]+)/") | .repo) + (.id|tostring)'
title: .name
blueprint: '"githubWorkflow"'
properties:
createdAt: .created_at
link: .html_url
path: .path
status: .state
updatedAt: .updated_at
relations:
repository: '.url | capture("repos/[^/]+/(?<repo>[^/]+)/") | .repo'
- kind: workflow-run
selector:
query: '(.head_branch | IN("main", "master", "production")) and ((.created_at | fromdateiso8601) > (now - 2592000))'
port:
entity:
mappings:
identifier: '.repository.full_name + (.id|tostring)'
title: .display_title
blueprint: '"githubWorkflowRun"'
properties:
conclusion: .conclusion
createdAt: .created_at
headBranch: .head_branch
link: .html_url
name: .name
runAttempt: .run_attempt
runNumber: .run_number
runStartedAt: .run_started_at
status: .status
triggeringActor: .triggering_actor.login
updatedAt: .updated_at
relations:
pullRequests: 'if (.pull_requests | length) > 0 then [.pull_requests[] | .head.repo.name + "-pr-" + (.number | tostring)] else null end'
repository: .repository.name
workflow: '.repository.full_name + (.workflow_id|tostring)'
service:
combinator: '"and"'
rules:
- property: '"github_repository_id"'
operator: '"="'
value: .repository.name
- kind: workflow-run
selector:
query: '(.head_branch | IN("main", "master", "production")) and ((.created_at | fromdateiso8601) > (now - 2592000))'
port:
entity:
mappings:
identifier: '.repository.full_name + (.workflow_id|tostring)'
title: '.repository.full_name + (.workflow_id|tostring)'
blueprint: '"githubWorkflow"'
properties:
last_triggered_at: .run_started_at
result: .conclusion
GitLab integration mapping - complete EI configuration (Click to expand)
resources:
- kind: group-with-members
selector:
query: "true"
includeBotMembers: "true"
includeInheritedMembers: "true"
port:
itemsToParse: .__members
entity:
mappings:
identifier: .item.username
title: .item.name
blueprint: '"gitlabMember"'
properties:
link: .item.web_url
state: .item.state
email: .item.email
locked: .item.locked
- kind: group-with-members
selector:
query: "true"
includeBotMembers: "true"
port:
entity:
mappings:
identifier: .full_path
title: .name
blueprint: '"gitlabGroup"'
properties:
url: .web_url
visibility: .visibility
description: .description
relations:
gitlabMembers: '.__members | map(.username)'
organization: '.full_path | split("/") | .[0]'
- kind: group-with-members
selector:
query: "true"
port:
entity:
mappings:
identifier: '.full_path | split("/") | .[0]'
title: '.full_path | split("/") | .[0]'
blueprint: '"gitlabOrganization"'
properties:
url: '(.web_url | split("/") | .[0:3] | join("/")) + "/" + (.full_path | split("/") | .[0])'
description: 'if (.full_path | split("/") | length) == 1 then .description else null end'
relations:
organization: '"default-org"'
- kind: project
selector:
query: "true"
includeLanguages: "true"
includedFiles:
- README.md
- CODEOWNERS
port:
entity:
mappings:
identifier: '.path_with_namespace | gsub(" "; "")'
title: .name
blueprint: '"gitlabRepository"'
properties:
url: .web_url
readme: '.__includedFiles["README.md"]'
codeowners: '.__includedFiles["CODEOWNERS"]'
description: .description
language: '.__languages // {} | to_entries | max_by(.value) | .key'
namespace: .namespace.full_path
fullPath: .path_with_namespace
defaultBranch: .default_branch
last_activity: .last_activity_at
- kind: merge-request
selector:
query: "true"
states:
- merged
- opened
updatedAfter: 90
includeOnlyActiveGroups: true
port:
entity:
mappings:
identifier: .id | tostring
title: .title
blueprint: '"gitlabMergeRequest"'
properties:
status: .state
createdAt: .created_at
updatedAt: .updated_at
mergedAt: .merged_at
link: .web_url
leadTimeHours: '(.created_at as $createdAt | .merged_at as $mergedAt | ($createdAt | sub("\\.\\d+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as $createdTimestamp | ($mergedAt | if . == null then null else sub("\\.\\d+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as $mergedTimestamp | if $mergedTimestamp == null then null else (((($mergedTimestamp - $createdTimestamp) / 3600) * 100 | floor) / 100) end)'
cycle_time_hours: '(.created_at as $createdAt | .merged_at as $mergedAt | ($createdAt | sub("\\.\\d+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as $createdTimestamp | ($mergedAt | if . == null then null else sub("\\.\\d+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as $mergedTimestamp | if $mergedTimestamp == null then null else (((($mergedTimestamp - $createdTimestamp) / 3600) * 100 | floor) / 100) end)'
has_reviewers: '(.reviewers | length) > 0'
has_assignees: '(.assignees | length) > 0'
relations:
repository: '.references.full // "" | gsub("!.+"; "")'
assignees_git_lab: '.assignees | map(.username)'
reviewers_git_lab: '.reviewers | map(.username)'
creator_git_lab: .author.username
reviewers:
combinator: '"and"'
rules:
- operator: '"in"'
property: '"git_lab_username"'
value: '.reviewers | map(.username)'
assignees:
combinator: '"and"'
rules:
- operator: '"in"'
property: '"git_lab_username"'
value: '.assignees | map(.username)'
creator:
combinator: '"and"'
rules:
- operator: '"="'
property: '"git_lab_username"'
value: .author.username
service:
combinator: '"and"'
rules:
- property: '"gitlab_repository_identifier"'
operator: '"="'
value: '.references.full // "" | gsub("!.+"; "")'
- kind: pipeline
selector:
query: '(.ref | IN("main", "master", "production")) and ((.created_at | sub("\\.\\d+Z$"; "Z") | fromdateiso8601) > (now - 2592000))'
port:
entity:
mappings:
identifier: .id | tostring
title: '(.__project.name // .__project.path_with_namespace) + " #" + (.id | tostring)'
blueprint: '"gitlabPipeline"'
properties:
status: .status
createdAt: .created_at
updatedAt: .updated_at
link: .web_url
source: .source
ref: .ref
relations:
repository: .__project.path_with_namespace
- kind: job
selector:
query: '(.pipeline.ref | IN("main", "master", "production")) and ((.created_at | sub("\\.\\d+Z$"; "Z") | fromdateiso8601) > (now - 2592000))'
port:
entity:
mappings:
identifier: .id | tostring
title: .name
blueprint: '"gitlabJob"'
properties:
status: .status
stage: .stage
startedAt: .started_at
finishedAt: .finished_at
link: .web_url
duration: .duration
relations:
pipeline: .pipeline.id | tostring
repository: '.web_url | split("/-/") | .[0] | split("://") | .[1] | split("/") | .[1:] | join("/")'
service:
combinator: '"and"'
rules:
- property: '"gitlab_repository_identifier"'
operator: '"="'
value: '.web_url | split("/-/") | .[0] | split("://") | .[1] | split("/") | .[1:] | join("/")'
Azure DevOps integration mapping - complete EI configuration (Click to expand)
resources:
- kind: project
selector:
query: "true"
defaultTeam: "true"
port:
entity:
mappings:
identifier: '.id | gsub(" "; "")'
title: .name
blueprint: '"azureDevopsProject"'
properties:
state: .state
revision: .revision
visibility: .visibility
defaultTeam: .defaultTeam.name
link: '.url | gsub("_apis/projects/"; "")'
relations:
organization: '"default-org"'
- kind: user
selector:
query: "true"
port:
entity:
mappings:
identifier: .user.mailAddress
title: .user.displayName
blueprint: '"azureDevopsUser"'
properties:
url: .user.url
- kind: team
selector:
query: "true"
includeMembers: true
port:
entity:
mappings:
identifier: .id
title: .name
blueprint: '"azureDevopsTeam"'
properties:
description: .description
url: .url
relations:
members: '.__members | map(.identity.uniqueName)'
project: '.projectId | gsub(" "; "")'
- kind: repository
selector:
query: "true"
port:
entity:
mappings:
identifier: .id
title: .name
blueprint: '"azureDevopsRepository"'
properties:
url: .remoteUrl
readme: file://README.md
codeowners: file://**/CODEOWNERS
id: .id
last_activity: .project.lastUpdateTime
relations:
project: '.project.id | gsub(" "; "")'
- kind: repository-policy
selector:
query: '.type.displayName=="Minimum number of reviewers"'
port:
entity:
mappings:
identifier: .__repository.id
blueprint: '"azureDevopsRepository"'
properties:
minimumApproverCount: .settings.minimumApproverCount
- kind: repository-policy
selector:
query: '.type.displayName=="Work item linking"'
port:
entity:
mappings:
identifier: .__repository.id
blueprint: '"azureDevopsRepository"'
properties:
workItemLinking: .isEnabled and .isBlocking
- kind: pull-request
selector:
query: "true"
minTimeInDays: 90
port:
entity:
mappings:
identifier: '.repository.id + "/" + (.pullRequestId | tostring)'
title: .title
blueprint: '"azureDevopsPullRequest"'
properties:
createdAt: .creationDate
cycle_time_hours: '(.creationDate as $createdAt | .status as $status | .closedDate as $closedAt | ($createdAt | sub("\\..*Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as $createdTimestamp | ($closedAt | if . == null then null else sub("\\..*Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as $closedTimestamp | if $status == "completed" and $closedTimestamp != null then (((($closedTimestamp - $createdTimestamp) / 3600) * 100 | floor) / 100) else null end)'
description: .description
has_reviewers: '(.reviewers | length) > 0'
link: '(.url | split("/_apis/")[0]) + "/_git/" + .repository.name + "/pullrequest/" + (.pullRequestId | tostring)'
status: .status
relations:
azure_devops_creator: .createdBy.uniqueName
azure_devops_reviewers: '[.reviewers[].uniqueName]'
repository: .repository.id
creator:
combinator: '"and"'
rules:
- property: '"azure_devops_user_id"'
operator: '"="'
value: .createdBy.uniqueName
reviewers:
combinator: '"and"'
rules:
- property: '"azure_devops_user_id"'
operator: '"in"'
value: '[.reviewers[].uniqueName]'
service:
combinator: '"and"'
rules:
- property: '"ado_repository_id"'
operator: '"="'
value: .repository.id
- kind: build
selector:
query: "true"
port:
entity:
mappings:
identifier: '.__project.id + "/" + (.id | tostring) | gsub(" "; "")'
title: .buildNumber
blueprint: '"azureDevopsBuild"'
properties:
definitionName: .definition.name
finishTime: .finishTime
link: ._links.web.href
queueTime: .queueTime
requestedFor: .requestedFor.displayName
result: .result
startTime: .startTime
status: .status
relations:
project: '.__project.id | gsub(" "; "")'
repository: .repository.id
service:
combinator: '"and"'
rules:
- property: '"ado_repository_id"'
operator: '"="'
value: .repository.id
- kind: pipeline-run
selector:
query: "true"
port:
entity:
mappings:
identifier: '.__project.id + "/" + (.__pipeline.id | tostring) + "/" + (.id | tostring) | gsub(" "; "")'
blueprint: '"azureDevopsPipelineRun"'
properties:
createdDate: .createdDate
finishedDate: .finishedDate
pipelineName: .pipeline.name
result: .result
state: .state
relations:
project: '.__project.id | gsub(" "; "")'
After saving the mapping configuration, trigger a resync of your integration to populate the catalog with data.
Verify ingestion
Once the resync completes, open the Software Catalog and confirm these entity types appear:
- GitHub (Ocean)
- GitLab
- Azure DevOps
| Blueprint | What you should see |
|---|---|
| GitHub Organization | One entity per top-level GitHub organization, with the organization relation pointing to default-org. |
| GitHub Team | Your GitHub teams, each with organization pointing to a githubOrganization entity. |
| GitHub User | GitHub users, each with the team relation populated where applicable. |
| GitHub Repository | Your GitHub repositories, with url, language, defaultBranch, last_push, visibility, and (where present) readme / codeowners populated. |
| GitHub Pull Request | Pull requests with status, createdAt, mergedAt, cycle_time_hours, and has_reviewers populated. The service and repository relations link each PR to its service and repository. |
| GitHub Workflow | GitHub Actions workflow definitions, each linked to their repository. |
| GitHub Workflow Run | Individual workflow run results with conclusion, status, createdAt, and runStartedAt populated. The service, repository, and workflow relations are populated. |
| Organization | The default-org entity you created earlier, now referenced by every githubOrganization entity. |
| Blueprint | What you should see |
|---|---|
| GitLab Namespace | One entity per top-level GitLab namespace, with the organization relation pointing to default-org. |
| GitLab Group | Your GitLab groups, each with organization pointing to a gitlabOrganization entity and gitlabMembers populated. |
| GitLab Member | Group members (users and bots, depending on selector settings). |
| GitLab Repository | Your GitLab projects, with url, language, defaultBranch, last_activity, and (where present) readme / codeowners populated. |
| GitLab Merge Request | Merge requests with status, createdAt, mergedAt, leadTimeHours, and cycle_time_hours populated. The service and repository relations are populated. |
| GitLab Pipeline | Pipeline runs with status, createdAt, ref, and source populated. Linked to their repository. |
| GitLab Job | Individual pipeline jobs with status, duration, stage, and startedAt. Linked to repository, service, and pipeline. |
| Organization | The default-org entity you created earlier, now referenced by every gitlabOrganization entity. |
| Blueprint | What you should see |
|---|---|
| Azure Project | One entity per Azure DevOps project, with the organization relation pointing to default-org. |
| Azure Devops Team | Your Azure DevOps teams, each with project and members populated. |
| Azure Devops User | Azure DevOps users discovered via team membership. |
| Azure Repository | Your Azure DevOps repositories, with url, last_activity, minimumApproverCount, workItemLinking, and (where present) readme / codeowners populated. The project relation links each repository to its azureDevopsProject. |
| Azure Pull Request | Pull requests with status, createdAt, cycle_time_hours, and has_reviewers populated. The service and repository relations are populated. |
| Azure Devops Pipeline Run | Pipeline run results with state, result, createdDate, and pipelineName. Linked to their project. |
| Azure Devops Build | Build results with status, result, startTime, finishTime, and definitionName. The service, project, and repository relations are populated. |
| Organization | The default-org entity you created earlier, now referenced by every azureDevopsProject entity. |
If any entity type above is absent after the resync, the corresponding kind is likely not enabled in your integration mapping. Open your integration's mapping config and verify the kind is present. For example, to enable pull request ingestion for GitHub, add a pull-request block:
resources:
- kind: pull-request
selector:
query: "true"
port:
entity:
mappings:
identifier: ".id | tostring"
title: ".title"
blueprint: '"githubPullRequest"'
properties: {}
relations: {}
Refer to the full default mapping in your integration's documentation: GitHub, GitLab, Azure DevOps.
Connect services to repositories
The mappings above populate provider-specific entities (organizations/namespaces/projects, teams, users, repositories), but they do not create service entities. You decide which repositories represent services in your catalog.
For each service:
-
Navigate to the Software Catalog.
-
Click + Service to create a new entity (or open an existing one to edit).
-
Set the repository relation that matches your SCM:
- GitHub repository → matching
githubRepositoryentity. - GitLab repository → matching
gitlabRepositoryentity. - Azure repository → matching
azureDevopsRepositoryentity.
The service's repository URL, README, language, default branch, and last activity populate automatically via mirror properties.
- GitHub repository → matching
-
Set the Teams field (Port's built-in
$teamownership) todefault-team(or any team you've created). -
Optionally set Criticality to one of
low,medium,high, orcritical.
You can also create services in bulk via the Port API or the Terraform provider once you have a mapping from repository to service.
What's next
With the foundational data model in place, you can now layer the Engineering Intelligence use cases on top. Each guide below assumes the core Organization, Team, and Service blueprints (with team ownership on services) plus the SCM blueprints described here are already set up.
- Measure PR/MR delivery metrics: Track cycle time, throughput, and stale PRs/MRs at the service, team, and organization levels.
- Set up DORA metrics: Track deployment frequency, lead time, change failure rate, and MTTR.
- Measure pipeline reliability: Track pipeline / workflow / build failure rates and trends across services and teams.
- Delivery performance scorecard: Grade services and teams on PR/MR delivery health.
- DORA scorecards: Grade services and teams against DORA benchmarks (Elite, High, Medium, Low).
- Pipeline reliability scorecard: Grade services on pipeline failure rates and stability.
- Production readiness scorecard: Grade services on baseline production-readiness criteria (ownership, README, CODEOWNERS, etc.).