Skip to content

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 GITHUB step targets the deploy.yml from Part 3 and forwards the two user inputs. The IDP adds port_run_id automatically.
  • record depends on the last deploy but has "condition": "ALWAYS" — the Deployments page shows failed deployments too, with status taken 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:

  • repos is an array of entities: the picker lists githubRepository entities most-recently-updated first (items.sort) and returns their identifiers — ["idpnextdemo/orders-service", …].
  • workflowInputs.repos strips the idpnextdemo/ prefix with jq before dispatch. GitHub workflow_dispatch inputs are strings, so the resulting array is sent JSON-encoded and the workflow parses it with fromJSON.
  • 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.dataset limits the picker to the three repositories that actually have a deploy.yml — a $identifier in […] rule — and items.sort orders them by title.
  • loop.items is the picked array; {{ .item }} inside the step config is one identifier, so repo resolves to orders-service, payments-service, … and every instance dispatches a different repository.
  • maxConcurrency: 2 — with three services picked, the run page shows Deploy (1/3) and (2/3) running while (3/3) stays PENDING.
  • record depends on deploy_service (all instances) and runs ALWAYS; {{ .steps.deploy_service.status }} is the aggregate — FAILURE if any deploy failed — and services lists what was deployed.
  • .user.teams holds the identifiers of the catalog _team entities the user belongs to — i.e. the team names from Admin → Users and teams. Create organization-wide admin and developer teams (or set IDP_ADMIN_TEAM / IDP_DEVELOPER_TEAM when 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, and release.dataset narrows 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.
  • githubRelease entities are keyed by the GitHub release id; the human-readable tag is a property, which is why the step reads .form.release.properties.tag rather 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 }} lets deploy-service.yml report back with the callback documented in Part 3.

Next: secrets & step credentials →

IDP Next — Internal Developer Platform