Appearance
The Demo Workflows
These are the complete, working definitions behind the demo's Self-service portal. Paste each into the workflow editor's JSON tab (or POST /api/v1/workflows) and adapt org/repo/project ids to your own. The first three are deployments; the next two showcase multi-entity inputs and looped steps; the last one chains two entity pickers (dependent inputs).
1. Deploy Stack (GitHub Actions)
A rolling deployment: three GitHub repositories deployed in sequence (orders → payments → frontend), then the result recorded as a deployment entity — the recording step runs ALWAYS, so failures are recorded too.
json
{
"identifier": "deploy-stack-github",
"title": "Deploy Stack (GitHub Actions)",
"icon": "LucideGithub",
"description": "Rolling deployment of the order stack: orders-service, then payments-service, then frontend-webapp — each via GitHub Actions in its own repository. Records a deployment entity.",
"category": "Deployment",
"kind": "WORKFLOW",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"required": ["environment", "version"],
"properties": {
"version": { "type": "string", "title": "Version", "default": "1.0.0" },
"environment": {
"enum": ["dev", "staging", "prod"],
"type": "string",
"title": "Environment",
"default": "staging"
}
}
},
"blueprintIdentifier": "deployment"
},
"published": true,
"steps": [
{
"identifier": "deploy_orders",
"title": "Deploy orders-service (GitHub)",
"order": 0,
"type": "GITHUB",
"config": {
"org": "idpnextdemo",
"ref": "main",
"repo": "orders-service",
"workflow": "deploy.yml",
"workflowInputs": {
"version": "{{ .inputs.version }}",
"environment": "{{ .inputs.environment }}"
}
},
"dependsOn": [],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "deploy_payments",
"title": "Deploy payments-service (GitHub)",
"order": 1,
"type": "GITHUB",
"config": {
"org": "idpnextdemo",
"ref": "main",
"repo": "payments-service",
"workflow": "deploy.yml",
"workflowInputs": {
"version": "{{ .inputs.version }}",
"environment": "{{ .inputs.environment }}"
}
},
"dependsOn": ["deploy_orders"],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "deploy_frontend",
"title": "Deploy frontend-webapp (GitHub)",
"order": 2,
"type": "GITHUB",
"config": {
"org": "idpnextdemo",
"ref": "main",
"repo": "frontend-webapp",
"workflow": "deploy.yml",
"workflowInputs": {
"version": "{{ .inputs.version }}",
"environment": "{{ .inputs.environment }}"
}
},
"dependsOn": ["deploy_orders", "deploy_payments"],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "record",
"title": "Record deployment",
"order": 3,
"type": "UPSERT_ENTITY",
"config": {
"mapping": {
"title": "Deploy {{ .inputs.version }} to {{ .inputs.environment }} (GitHub)",
"identifier": "{{ .run.id }}",
"properties": {
"url": "https://idpnext.wazyneo.com/self-service/runs/{{ .run.id }}",
"status": "{{ .steps.deploy_frontend.status }}",
"version": "{{ .inputs.version }}",
"provider": "github",
"services": ["orders-service", "payments-service", "frontend-webapp"],
"workflow": "Deploy Stack (GitHub Actions)",
"environment": "{{ .inputs.environment }}",
"triggeredBy": "{{ .user.email }}"
}
},
"blueprintIdentifier": "deployment"
},
"dependsOn": ["deploy_frontend"],
"condition": "ALWAYS",
"timeoutMinutes": 10
}
]
}Points to notice:
- Each
GITHUBstep targets thedeploy.ymlfrom Part 3 and forwards the two user inputs. The IDP addsport_run_idautomatically. recorddepends on the last deploy but has"condition": "ALWAYS"— the Deployments page shows failed deployments too, withstatustaken from{{ .steps.deploy_frontend.status }}.
2. Deploy Stack (GitLab CI)
Same idea with GitLab: analytics first, then orders and payments in parallel (both depend only on deploy_analytics). The recorded status is computed with a jq conditional over both parallel steps.
json
{
"identifier": "deploy-stack-gitlab",
"title": "Deploy Stack (GitLab CI)",
"icon": "LucideGitBranch",
"description": "Deploys analytics-pipeline first, then orders-service and payments-service in parallel — each via a GitLab CI pipeline in its own project. Records a deployment entity.",
"category": "Deployment",
"kind": "WORKFLOW",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"required": ["environment", "version"],
"properties": {
"version": { "type": "string", "title": "Version", "default": "1.0.0" },
"environment": {
"enum": ["dev", "staging", "prod"],
"type": "string",
"title": "Environment",
"default": "staging"
}
}
},
"blueprintIdentifier": "deployment"
},
"published": true,
"steps": [
{
"identifier": "deploy_analytics",
"title": "Deploy analytics-pipeline (GitLab)",
"order": 0,
"type": "GITLAB",
"config": {
"ref": "main",
"projectId": 84432928,
"pipelineVariables": {
"DEPLOY_ENV": "{{ .inputs.environment }}",
"DEPLOY_VERSION": "{{ .inputs.version }}"
}
},
"dependsOn": [],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "deploy_orders",
"title": "Deploy orders-service (GitLab)",
"order": 1,
"type": "GITLAB",
"config": {
"ref": "main",
"projectId": 84432919,
"pipelineVariables": {
"DEPLOY_ENV": "{{ .inputs.environment }}",
"DEPLOY_VERSION": "{{ .inputs.version }}"
}
},
"dependsOn": ["deploy_analytics"],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "deploy_payments",
"title": "Deploy payments-service (GitLab)",
"order": 2,
"type": "GITLAB",
"config": {
"ref": "main",
"projectId": 84432915,
"pipelineVariables": {
"DEPLOY_ENV": "{{ .inputs.environment }}",
"DEPLOY_VERSION": "{{ .inputs.version }}"
}
},
"dependsOn": ["deploy_analytics"],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "record",
"title": "Record deployment",
"order": 3,
"type": "UPSERT_ENTITY",
"config": {
"mapping": {
"title": "Deploy {{ .inputs.version }} to {{ .inputs.environment }} (GitLab)",
"identifier": "{{ .run.id }}",
"properties": {
"url": "https://idpnext.wazyneo.com/self-service/runs/{{ .run.id }}",
"status": "{{ if (.steps.deploy_orders.status == \"SUCCESS\") and (.steps.deploy_payments.status == \"SUCCESS\") then \"SUCCESS\" else \"FAILURE\" end }}",
"version": "{{ .inputs.version }}",
"provider": "gitlab",
"services": ["analytics-pipeline", "orders-service", "payments-service"],
"workflow": "Deploy Stack (GitLab CI)",
"environment": "{{ .inputs.environment }}",
"triggeredBy": "{{ .user.email }}"
}
},
"blueprintIdentifier": "deployment"
},
"dependsOn": ["deploy_orders", "deploy_payments"],
"condition": "ALWAYS",
"timeoutMinutes": 10
}
]
}projectId is the numeric GitLab project id (Project → Settings → General, or GET /api/v4/groups/<group>/projects).
3. Full-Stack Release (GitHub + GitLab)
The governance showcase: an approval gate, then a GitHub deployment, then a GitLab deployment, then recording. The GitHub step authenticates with a stored secret instead of the server environment.
json
{
"identifier": "release-full-stack",
"title": "Full-Stack Release (GitHub + GitLab)",
"icon": "LucideRocket",
"description": "Approval-gated release: deploys the backend via GitHub Actions (orders-service), then the frontend via a GitLab CI pipeline (frontend-webapp). Records a deployment entity.",
"category": "Deployment",
"kind": "WORKFLOW",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"required": ["environment", "version"],
"properties": {
"version": { "type": "string", "title": "Version", "default": "1.0.0" },
"environment": {
"enum": ["dev", "staging", "prod"],
"type": "string",
"title": "Environment",
"default": "prod"
}
}
},
"blueprintIdentifier": "deployment"
},
"published": true,
"steps": [
{
"identifier": "approve",
"title": "Release approval",
"order": 0,
"type": "APPROVAL",
"config": {
"approvers": [],
"onTimeout": "approve",
"timeoutMinutes": 5
},
"dependsOn": [],
"condition": "ON_SUCCESS",
"timeoutMinutes": 5
},
{
"identifier": "deploy_backend",
"title": "Deploy orders-service backend (GitHub)",
"order": 1,
"type": "GITHUB",
"config": {
"org": "idpnextdemo",
"ref": "main",
"repo": "orders-service",
"workflow": "deploy.yml",
"credentials": {
"source": "secret",
"version": "latest",
"secretIdentifier": "idpnetxdemo-github-app"
},
"workflowInputs": {
"version": "{{ .inputs.version }}",
"environment": "{{ .inputs.environment }}"
}
},
"dependsOn": ["approve"],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "deploy_frontend",
"title": "Deploy frontend-webapp (GitLab)",
"order": 2,
"type": "GITLAB",
"config": {
"ref": "main",
"projectId": 84432930,
"pipelineVariables": {
"DEPLOY_ENV": "{{ .inputs.environment }}",
"DEPLOY_VERSION": "{{ .inputs.version }}"
}
},
"dependsOn": ["deploy_backend"],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "record",
"title": "Record deployment",
"order": 3,
"type": "UPSERT_ENTITY",
"config": {
"mapping": {
"title": "Release {{ .inputs.version }} to {{ .inputs.environment }} (Full stack)",
"identifier": "{{ .run.id }}",
"properties": {
"url": "https://idpnext.wazyneo.com/self-service/runs/{{ .run.id }}",
"status": "{{ .steps.deploy_frontend.status }}",
"version": "{{ .inputs.version }}",
"provider": "mixed",
"services": ["orders-service", "frontend-webapp"],
"workflow": "Full-Stack Release (GitHub + GitLab)",
"environment": "{{ .inputs.environment }}",
"triggeredBy": "{{ .user.email }}"
}
},
"blueprintIdentifier": "deployment"
},
"dependsOn": ["deploy_frontend"],
"condition": "ALWAYS",
"timeoutMinutes": 10
}
]
}The APPROVAL step config:
approvers: []— any user who can see the run may approve or decline. List user emails to restrict it.onTimeout: "approve"+timeoutMinutes: 5— if nobody decides within 5 minutes, the release auto-approves (demo-friendly; in production you'd typically use"reject").
Approvals need a human
Service accounts cannot approve APPROVAL steps ("Failed to record approval decision") — approvals must come from a real user, or from the onTimeout policy.
4. Bulk Repo Audit (multi-entity input)
A single-step action (kind: "ACTION") filed under its own Repository Ops category: the user picks any number of repositories and one GitHub Actions run audits them all through its own matrix.
json
{
"identifier": "bulk-repo-audit",
"title": "Bulk Repo Audit",
"icon": "LucideGithub",
"description": "Pick any number of GitHub repositories and run a read-only audit across all of them in a single GitHub Actions run. Demonstrates the multi-entity self-service input (type: array, items: { format: \"entity\" }).",
"category": "Repository Ops",
"kind": "ACTION",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"properties": {
"repos": {
"type": "array",
"title": "Repositories",
"description": "Pick one or more repositories to audit",
"items": {
"type": "string",
"format": "entity",
"blueprint": "githubRepository",
"sort": { "property": "$updatedAt", "order": "DESC" }
}
},
"note": { "type": "string", "title": "Note", "description": "Optional note included in the run summary" }
},
"required": ["repos"],
"order": ["repos", "note"]
}
},
"published": true,
"steps": [
{
"identifier": "bulk_audit",
"title": "Run bulk audit (GitHub Actions)",
"order": 0,
"type": "GITHUB",
"config": {
"org": "idpnextdemo",
"repo": "deployments-config-v2",
"workflow": "bulk-repo-action.yml",
"ref": "main",
"workflowInputs": {
"repos": "{{ .inputs.repos | map(split(\"/\") | last) }}",
"note": "{{ .inputs.note }}",
"triggered_by": "{{ .user.email }}"
}
},
"dependsOn": [],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
}
]
}Points to notice:
reposis an array of entities: the picker listsgithubRepositoryentities most-recently-updated first (items.sort) and returns their identifiers —["idpnextdemo/orders-service", …].workflowInputs.reposstrips theidpnextdemo/prefix with jq before dispatch. GitHubworkflow_dispatchinputs are strings, so the resulting array is sent JSON-encoded and the workflow parses it withfromJSON.category: "Repository Ops"gives the Self-service page a second category, so the filter chips appear.
The target workflow, .github/workflows/bulk-repo-action.yml in deployments-config-v2, fans out with a matrix:
yaml
name: Bulk Repo Action
on:
workflow_dispatch:
inputs:
repos:
description: 'JSON array of repository identifiers selected in idpnext'
required: true
type: string
note:
required: false
type: string
default: ''
triggered_by:
required: false
type: string
default: ''
jobs:
audit:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
repo: ${{ fromJSON(inputs.repos) }}
steps:
- name: Audit ${{ matrix.repo }}
env:
GH_TOKEN: ${{ secrets.PAT_GITHUB_TOKEN }}
run: |
gh api "repos/idpnextdemo/${{ matrix.repo }}" --jq '{name, default_branch, visibility, pushed_at, open_issues_count}'5. Deploy Selected Services (per-repo loop)
The same multi-entity input, but this time the IDP does the fan-out: one GITHUB step with loop becomes one instance per selected repository, each dispatching that repository's own deploy.yml from Part 3, at most two at a time. The record step waits for all of them and writes the aggregate result to the Deployments page.
The Environment options are resolved per user at runtime from the teams they belong to: enum is a jqQuery over .user.teams, so members of the admin team see every environment (dev / int / staging / prod), members of developer see dev / staging, and anyone else gets an empty (disabled) dropdown. default reuses the same expression and takes the first entry. The same list is enforced again when the run starts, so a value that was not offered is rejected.
json
{
"identifier": "deploy-selected-services",
"title": "Deploy Selected Services",
"icon": "LucideLayers",
"description": "Pick which services to deploy: one GitHub Actions deploy runs per selected repository (at most two at a time), then the result is recorded as a deployment entity. Demonstrates a per-item step loop over a multi-entity input.",
"category": "Deployment",
"kind": "WORKFLOW",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"blueprintIdentifier": "deployment",
"userInputs": {
"properties": {
"repos": {
"type": "array",
"title": "Services",
"description": "Each selected repository gets its own deploy run",
"items": {
"type": "string",
"format": "entity",
"blueprint": "githubRepository",
"sort": { "property": "$title", "order": "ASC" },
"dataset": {
"combinator": "and",
"rules": [
{
"property": "$identifier",
"operator": "in",
"value": ["idpnextdemo/orders-service", "idpnextdemo/payments-service", "idpnextdemo/frontend-webapp"]
}
]
}
}
},
"environment": {
"type": "string",
"title": "Environment",
"enum": {
"jqQuery": "if any(.user.teams[]?; . == \"admin\") then [\"dev\",\"int\",\"staging\",\"prod\"] elif any(.user.teams[]?; . == \"developer\") then [\"dev\",\"staging\"] else [] end"
},
"default": {
"jqQuery": "if any(.user.teams[]?; . == \"admin\") then [\"dev\",\"int\",\"staging\",\"prod\"] elif any(.user.teams[]?; . == \"developer\") then [\"dev\",\"staging\"] else [] end | first"
}
},
"version": { "type": "string", "title": "Version", "default": "1.0.0" }
},
"required": ["repos", "environment", "version"],
"order": ["repos", "environment", "version"]
}
},
"published": true,
"steps": [
{
"identifier": "deploy_service",
"title": "Deploy",
"order": 0,
"type": "GITHUB",
"loop": { "items": "{{ .inputs.repos }}", "maxConcurrency": 2 },
"config": {
"org": "idpnextdemo",
"repo": "{{ .item | split(\"/\") | last }}",
"workflow": "deploy.yml",
"ref": "main",
"workflowInputs": {
"environment": "{{ .inputs.environment }}",
"version": "{{ .inputs.version }}"
}
},
"dependsOn": [],
"condition": "ON_SUCCESS",
"timeoutMinutes": 10
},
{
"identifier": "record",
"title": "Record deployment",
"order": 1,
"type": "UPSERT_ENTITY",
"config": {
"blueprintIdentifier": "deployment",
"mapping": {
"identifier": "{{ .run.id }}",
"title": "Deploy {{ .inputs.version }} to {{ .inputs.environment }} ({{ .inputs.repos | length }} services)",
"properties": {
"url": "https://idpnext.wazyneo.com/self-service/runs/{{ .run.id }}",
"status": "{{ .steps.deploy_service.status }}",
"version": "{{ .inputs.version }}",
"provider": "github",
"services": "{{ .inputs.repos | map(split(\"/\") | last) }}",
"workflow": "Deploy Selected Services",
"environment": "{{ .inputs.environment }}",
"triggeredBy": "{{ .user.email }}"
}
}
},
"dependsOn": ["deploy_service"],
"condition": "ALWAYS",
"timeoutMinutes": 10
}
]
}Points to notice:
items.datasetlimits the picker to the three repositories that actually have adeploy.yml— a$identifier in […]rule — anditems.sortorders them by title.loop.itemsis the picked array;{{ .item }}inside the step config is one identifier, soreporesolves toorders-service,payments-service, … and every instance dispatches a different repository.maxConcurrency: 2— with three services picked, the run page showsDeploy (1/3)and(2/3)running while(3/3)staysPENDING.recorddepends ondeploy_service(all instances) and runsALWAYS;{{ .steps.deploy_service.status }}is the aggregate —FAILUREif any deploy failed — andserviceslists what was deployed..user.teamsholds the identifiers of the catalog_teamentities the user belongs to — i.e. the team names from Admin → Users and teams. Create organization-wideadminanddeveloperteams (or setIDP_ADMIN_TEAM/IDP_DEVELOPER_TEAMwhen running the provisioning script) and add people to them to control who may deploy where; there is no per-user configuration in the workflow itself.
6. Deploy Service (dependent entity pickers)
Two entity inputs that depend on each other: pick a service (service blueprint, synced from apps/services/*/catalog.yml in deployments-config-v2), then a release — the second picker only lists githubRelease entities of that service's repository. The GITHUB step then forwards plain values to deploy-service.yml in deployments-config-v2: the repository (.form.service.relations.repository) and the tag (.form.release.properties.tag), so the GitHub workflow needs no lookup of its own.
The blueprints, the Ocean mapping that fills them and this workflow definition are all applied from JSON by the github-ingestion scripts (apply.sh), not through the UI.
json
{
"identifier": "deploy-service",
"title": "Deploy Service",
"icon": "Rocket",
"description": "Deploy a release of a service to an environment. The release picker only lists releases of the selected service's repository. Dispatches deploy-service.yml in idpnextdemo/deployments-config-v2 with the repository and the release tag.",
"category": "Deployment",
"kind": "WORKFLOW",
"trigger": {
"type": "self-service",
"operation": "CREATE",
"userInputs": {
"properties": {
"service": {
"type": "string",
"format": "entity",
"blueprint": "service",
"title": "Service",
"description": "Service declared in deployments-config-v2 (apps/services/<name>/catalog.yml)",
"sort": {
"property": "$title",
"order": "ASC"
}
},
"release": {
"type": "string",
"format": "entity",
"blueprint": "githubRelease",
"title": "Release",
"description": "GitHub release of the selected service's repository",
"dependsOn": [
"service"
],
"dataset": {
"combinator": "and",
"rules": [
{
"relation": "repository",
"operator": "=",
"value": {
"jqQuery": ".form.service.relations.repository"
}
}
]
},
"sort": {
"property": "publishedAt",
"order": "DESC"
}
},
"environment": {
"type": "string",
"title": "Environment",
"enum": [
"dev",
"staging",
"prod"
],
"default": "dev"
}
},
"required": [
"service",
"release",
"environment"
],
"order": [
"service",
"release",
"environment"
]
}
},
"steps": [
{
"identifier": "deploy",
"title": "Deploy via GitHub Actions",
"order": 0,
"type": "GITHUB",
"config": {
"org": "idpnextdemo",
"repo": "deployments-config-v2",
"workflow": "deploy-service.yml",
"ref": "main",
"workflowInputs": {
"environment": "{{ .inputs.environment }}",
"service": "{{ .inputs.service }}",
"repository": "{{ .form.service.relations.repository }}",
"tag": "{{ .form.release.properties.tag }}",
"requested_by": "{{ .user.email }}",
"port_run_id": "{{ .step.id }}"
},
"credentials": {
"source": "secret",
"secretIdentifier": "github-actions-token"
}
},
"dependsOn": [],
"condition": "ON_SUCCESS",
"timeoutMinutes": 30
}
]
}Points to notice:
release.dependsOn: ["service"]keeps the release picker disabled until a service is chosen, andrelease.datasetnarrows it with a relation rule:relation: "repository"equals.form.service.relations.repository— the selected service as a hydrated entity, see advanced form → chaining entity inputs.githubReleaseentities are keyed by the GitHub release id; the human-readable tag is a property, which is why the step reads.form.release.properties.tagrather than.inputs.release..form.<input>is snapshotted when the run starts, so the step sees the same entity the form validated against.port_run_id: {{ .step.id }}letsdeploy-service.ymlreport back with the callback documented in Part 3.
