Platform engineering · October 2026

From IDP to agentic developer platform: golden paths as contracts agents can run

Internal developer platforms were designed for people clicking through a portal. More and more of the callers are now coding agents, review bots and AI SRE tools. This is how I'd turn golden paths into contracts those agents can run, without giving them a way around the guardrails the platform exists to enforce.

· 16 min read · Arjun Kanojia

In my current role at Deutsche Telekom Digital Labs I work on an internal developer platform built from fairly standard parts: Backstage software templates, Crossplane for infrastructure, Argo CD for GitOps, Kargo for promotions between environments, and multi-tenant EKS where every team gets observability by default. I also use agentic AI every week to create and maintain Terraform.

Put those two things side by side and one question keeps coming up. The platform has carefully designed golden paths for humans. The agents don't use them. They write YAML and HCL straight into repositories, skip the template, and leave reviewers to work out whether the result matches what the golden path would have produced. That's backwards. The golden path should be the easiest route for an agent too, and the safest.

This post is the design I'd use to get there. None of it needs a new product. It's mostly about treating each golden path as a contract with a strict input schema, deterministic checks and a clear owner, and then letting agents call that contract instead of improvising.

1. Why agents change who your platform is for

The 2025 DORA report found that 90% of respondents use AI at work, and that AI adoption now goes with higher delivery throughput but lower delivery stability. DORA's summary is that AI is an amplifier: it makes a good system faster and a weak one more fragile. The same report says 90% of organisations now have at least one internal platform, and DORA's AI Capabilities Model names "quality internal platforms" as one of seven capabilities that amplify AI's positive effect, because the platform supplies the guardrails and shared capabilities that let AI scale safely.

Read that from a platform team's side. If agents write more changes, faster, the platform is the only place you can put consistent controls in front of all of them. Code review alone won't hold when the change volume doubles.

The industry is converging on a name for this. PlatformEngineering.org is running a course this month on evolving an IDP into an "agentic development platform", built around golden paths as machine-executable contracts and a loop of a probabilistic step followed by a deterministic gate. KubeCon North America in Salt Lake City (9 to 12 November) has a Platform Engineering track and a new AI Inference + Agentic track. The tooling has caught up too: Backstage can now expose plugin actions over MCP, Crossplane v2 makes namespaced composite resources the default, and Kargo has pull-request and approval steps built into promotions.

2. A golden path is a contract, not a wizard

Most golden paths today are wizards. A form asks a few questions, a template renders some files, and the knowledge of what's allowed lives partly in the form and partly in the heads of the platform team. A human fills in the gaps. An agent will fill them in too, just not always correctly.

A contract makes the gaps explicit. For each path you write down what it accepts, what it guarantees, how it's checked and who owns it. The CNCF Platform Engineering Maturity Model already pushes platforms towards self-service through well-defined interfaces. Agents just make that requirement strict.

AspectGolden path as a wizard (for humans)Golden path as a contract (for humans and agents)
InputsForm fields, loose validation, defaults in the UIA versioned schema with types, enums, ranges and cross-field rules
GuaranteesImplied by the templateWritten down: "critical tier means Multi-AZ, 14-day backups, deletion protection"
ChecksCode reviewDeterministic gates in CI that return machine-readable errors
Side effectsSometimes direct API calls from the portalOnly a Git merge request; GitOps applies it
DiscoveryA catalogue page people browseA tool with a name, description and schema that an agent can list
Ownership"The platform team"A named owning team, an SLO for the path, a changelog
AutonomyNot a questionSet per path: draft only, run with approval, or auto-merge within policy

If that looks like an API, it is. The useful mental shift is that your platform already has an API, it's just spread across templates, Helm charts, Terraform modules and wiki pages. Agents force you to make it a real one.

3. The three-layer architecture

Here's the reference layout I'd draw. At the bottom are the usual IDP planes. On top of them sits a thin layer of command paths, which are the golden path contracts. On top of that sits agent infrastructure, the part that's new. Humans keep using the portal; agents come in through a tool gateway, and both end up in the same command paths.

CALLERS DeveloperBackstage portal UI IDE coding agentwrites app + config changes CI / review agentfixes failed gates, upgrades AI SRE agentrunbook actions only AGENT INFRASTRUCTURE (new) Tool gatewayMCP, allowlist Agent identityOIDC, per agent PolicyOPA, autonomy Contextcatalog, docs Traces + evalsOTel COMMAND PATHS · GOLDEN PATH CONTRACTS Backstage templates+ actions registry Crossplane XRsschema = contract Kargo promotionsPR, approval, verify Runbook actionsreviewed catalogue Git merge request is the only write path IDP PLANES (existing) Developercontrol planeBackstage, Git Integration+ deliveryGitLab CI, Argo CD ResourceplaneEKS, AWS Monitoring+ loggingon by default Securityplanescan, KMS, policy humans (dashed blue) skip the gateway; agents never skip the command paths
Fig 1. An agentic developer platform is an IDP with two thin layers on top: golden path contracts that both humans and agents call, and agent infrastructure that only agents pass through.

Three decisions in that picture do most of the work:

  • Agents never talk to the resource plane directly. No cloud credentials, no kubeconfig with write access. If an agent needs a database, it calls the database contract, which produces a merge request. This is the same principle I used in my post on Terraform guardrails, applied to the whole platform.
  • Git is the only write path. Argo CD and Crossplane reconcile what's merged. That gives you review, history, rollback and a single audit trail for free, whether the author was a person or an agent.
  • The agent infrastructure layer is thin and boring. A gateway, identities, a policy engine, context and tracing. It doesn't contain business logic. The logic stays in the contracts, where the platform team already owns it.

4. Exposing golden paths as tools

For an agent to use a golden path, it has to find it and understand its inputs. That means turning paths into tools with names, descriptions and schemas. Backstage now does a lot of this for you. Plugins register actions in the actions registry with Zod input and output schemas plus attributes like readOnly, idempotent and destructive, and the MCP actions backend serves them to MCP clients over Streamable HTTP.

The defaults matter here. Per the Backstage docs, an action with no attributes set is treated as not read-only and not idempotent, and destructive defaults to true unless the action is read-only. So an unlabelled action is assumed dangerous, which is exactly what you want. You can then split actions into separate MCP servers and filter out anything destructive:

# app-config.yaml: expose catalog and scaffolder actions to agents, never destructive ones
backend:
  actions:
    pluginSources:
      - catalog
      - scaffolder
mcpActions:
  servers:
    catalog:                         # served at /api/mcp-actions/v1/catalog
      filter:
        include:
          - id: 'catalog:*'
        exclude:
          - attributes:
              destructive: true      # exclude rules win over include rules
    scaffolder:                      # served at /api/mcp-actions/v1/scaffolder
      filter:
        include:
          - id: 'scaffolder:*'
        exclude:
          - attributes:
              destructive: true

Two practical notes. First, authentication: the Backstage docs describe static tokens for MCP clients as a temporary option and recommend OAuth with Client ID Metadata Documents; Dynamic Client Registration is now deprecated there. Give each agent its own identity rather than one shared token, or your audit log will say "mcp-clients" for everything. Second, keep the tool list short. An agent with forty overlapping tools picks the wrong one more often than an agent with eight clear ones. Name tools after outcomes ("create-postgres-database"), not mechanics ("run-template").

The template behind a tool is where the contract starts. This one adds a database claim to the team's GitOps folder through a merge request, and nothing else:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: postgres-database
  title: PostgreSQL database for a service
  description: Adds a Database claim for one service via a merge request. Never applies directly.
  tags: [recommended, crossplane, agent-callable]
spec:
  owner: group:platform-team
  type: resource
  parameters:
    - title: Database
      required: [service, tier, sizeGiB]
      properties:
        service: { type: string, pattern: '^[a-z][a-z0-9-]{2,40}$' }
        tier:    { type: string, enum: [standard, critical] }
        sizeGiB: { type: integer, minimum: 20, maximum: 500 }
  steps:
    - id: render
      name: Render claim
      action: fetch:template
      input:
        url: ./claim
        targetPath: ./out
        values:
          service: ${{ parameters.service }}
          tier: ${{ parameters.tier }}
          sizeGiB: ${{ parameters.sizeGiB }}
    - id: mr
      name: Open merge request
      action: publish:gitlab:merge-request
      input:
        repoUrl: gitlab.example.internal?owner=platform&repo=gitops
        branchName: db-${{ parameters.service }}
        title: 'Add database for ${{ parameters.service }}'
        description: 'Created from the postgres-database golden path.'
        sourcePath: ./out
        targetPath: teams/${{ parameters.service }}/db
  output:
    links:
      - { title: Merge request, url: ${{ steps.mr.output.mergeRequestUrl }} }

Notice what's missing: no free-text field for "extra settings", no raw Helm values, no instance class. Every field an agent can set is one the platform team has decided is safe to vary. That's the contract.

5. Put the rules in the schema

Template parameters are only the front door. If someone, human or agent, writes the claim YAML by hand and skips Backstage, the template's validation doesn't run. So the real contract has to live in the API the cluster enforces. With Crossplane, that's the composite resource definition.

In Crossplane v2, XRDs use apiextensions.crossplane.io/v2 and composite resources can be namespaced, which fits multi-tenant clusters where each team owns a namespace. Because an XRD becomes a CRD, you also get Kubernetes CEL validation rules for cross-field checks:

apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
  name: databases.platform.example.org
spec:
  scope: Namespaced                     # one namespace per team on multi-tenant EKS
  group: platform.example.org
  names:
    kind: Database
    plural: databases
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: [tier, sizeGiB]
              properties:
                tier:
                  type: string
                  enum: [standard, critical]
                  description: "critical = Multi-AZ, 14-day backups, deletion protection"
                sizeGiB:
                  type: integer
                  minimum: 20
                  maximum: 500
                engineVersion:
                  type: string
                  enum: ["16", "17"]
                  default: "17"
              x-kubernetes-validations:
                - rule: "self.tier != 'critical' || self.sizeGiB >= 50"
                  message: "critical databases start at 50 GiB"
            status:
              type: object
              properties:
                endpoint: { type: string }

The description fields aren't decoration. They're what an agent reads when it inspects the API, so write them for a reader who has never seen your wiki. The Composition behind this XRD decides what "critical" really means in AWS terms (Multi-AZ RDS, backup retention, deletion protection, a customer-managed KMS key), and none of those knobs are exposed. The agent chooses the tier; the platform chooses the implementation.

A good test for any golden path: could an agent that has read only the schema and its descriptions produce a valid request on the first try? If not, the knowledge is still in someone's head, and it'll leak out as review comments.

6. The validation loop: probabilistic step, deterministic gate

Agents are probabilistic. They'll produce a slightly different claim each time and occasionally a wrong one. You don't fix that by asking the model to be more careful. You fix it by putting a deterministic gate after every probabilistic step, returning the gate's errors to the agent in a structured form, and letting it retry a bounded number of times before a human takes over.

Intentticket or prompt Agent drafts changeprobabilistic step DETERMINISTIC GATES (same for humans) 1 schema + CEL rules (XRD, template)2 render (crossplane render, helm template)3 validate (kubeconform -strict)4 policy (conftest / OPA)5 diff + blast radius (Argo CD diff) fail: errors as JSON back to agent, ≤ 3 tries 3rd failure Escalate to ownerswith the draft + gate output pass Merge requestapproval by autonomy level Kargo promotion+ verification every run traced, tagged agent or human, scored
Fig 2. The validation loop. The agent can be wrong; the gates can't be skipped. The same gates run for human merge requests, so there's one definition of "valid".

The gates themselves are ordinary CI. The point is that they run on the rendered output, not just the input, and that they produce machine-readable results the agent can act on. Here's a GitLab CI job for a Crossplane claim:

# .gitlab-ci.yml: the gate every golden path change goes through, agent or human
validate-claims:
  stage: validate
  image: registry.example.internal/platform/gates:2026.10   # crossplane CLI, kubeconform, conftest
  services: [docker:dind]                                     # crossplane render runs functions in containers
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes: ["teams/**/*.yaml"]
  script:
    - |
      for claim in $(git diff --name-only "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- 'teams/**/*.yaml'); do
        crossplane render "$claim" platform/compositions/database.yaml platform/functions.yaml \
          --include-full-xr >> rendered.yaml
        echo '---' >> rendered.yaml
      done
    - kubeconform -strict -summary -schema-location default
        -schema-location 'schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' rendered.yaml
    - conftest test rendered.yaml --policy policy/ --output json | tee conftest.json
  artifacts:
    when: always
    paths: [rendered.yaml, conftest.json]                       # the agent reads these on failure

And a policy that encodes things the schema can't, written for OPA 1.x and run with conftest:

package main

# every rendered resource carries an owner, so cost and incidents route to a team
deny contains msg if {
  input.metadata
  not input.metadata.labels["platform.example.org/owner"]
  msg := sprintf("%s/%s: missing platform.example.org/owner label", [input.kind, input.metadata.name])
}

# critical databases must keep 14 days of backups, whatever the composition did
deny contains msg if {
  input.kind == "Instance"                     # RDS instance managed resource
  input.metadata.labels["platform.example.org/tier"] == "critical"
  input.spec.forProvider.backupRetentionPeriod < 14
  msg := sprintf("%s: critical tier needs backupRetentionPeriod >= 14", [input.metadata.name])
}

A few rules I'd hold the loop to:

  • Bound the retries. Three attempts is plenty. An agent that can't pass the gates in three tries is either missing context or the contract is unclear. Both are worth a human looking at, and both are useful feedback for the platform team.
  • Make errors actionable. "Policy failed" is useless to an agent. "critical tier needs backupRetentionPeriod >= 14" gets fixed on the next attempt. Good gate messages help humans just as much.
  • Gates are not optional for anyone. If a human can bypass a gate with a label, an agent will eventually be told to add that label. Keep emergency overrides, but make them rare, logged and owned by people.

7. Promotion: where agent changes meet production

Passing the gates means a change is valid. It doesn't mean it's safe to ship to production. That decision belongs to the promotion layer, and Kargo is a good fit because promotions are declarative steps you can inspect. Its Stages pull Freight from the stage before, write the change into Git, can open a pull request and wait for it to merge, update Argo CD and then run verification.

apiVersion: kargo.akuity.io/v1alpha1
kind: Stage
metadata:
  name: prod
  namespace: payments
spec:
  requestedFreight:
    - origin: { kind: Warehouse, name: payments-api }
      sources: { stages: [staging] }        # only Freight that survived staging
  promotionTemplate:
    spec:
      vars:
        - { name: gitopsRepo, value: https://gitlab.example.internal/platform/gitops.git }
      steps:
        - uses: git-clone
          config:
            repoURL: ${{ vars.gitopsRepo }}
            checkout: [{ branch: main, path: ./out }]
        - uses: yaml-update
          config:
            path: ./out/apps/payments-api/prod/values.yaml
            updates:
              - key: image.tag
                value: ${{ imageFrom("registry.example.internal/payments-api").Tag }}
        - uses: git-commit
          config: { path: ./out, message: "promote payments-api to prod" }
        - uses: git-push
          as: push
          config: { path: ./out, generateTargetBranch: true }
        - uses: git-open-pr                        # a human approves the merge
          as: open-pr
          config:
            repoURL: ${{ vars.gitopsRepo }}
            sourceBranch: ${{ outputs.push.branch }}
            targetBranch: main
            title: "Promote payments-api to prod"
            labels: [promotion, needs-review]
        - uses: git-wait-for-pr
          config:
            repoURL: ${{ vars.gitopsRepo }}
            prNumber: ${{ outputs['open-pr'].pr.id }}
        - uses: argocd-update
          config:
            apps: [{ name: payments-api-prod }]
  verification:
    analysisTemplates:
      - name: slo-burn-check                   # Argo Rollouts AnalysisTemplate

Why this pattern works well with agents:

  • Freight only moves forward. An agent can't promote something to prod that never ran in staging, because the Stage only accepts Freight verified upstream.
  • The approval lives in the promotion, not in the agent. The pull request is the gate. If you run Kargo on the Akuity Platform, v1.12 and later also have a wait-for-approval step with quorum rules, like two approvers with one from a named group.
  • Argo CD stays in charge of the cluster. The argocd-update step only works on Applications annotated to allow that Stage, and Argo CD sync windows still block syncs during freezes. Agents inherit your change freeze automatically.
  • Verification decides, not the agent's confidence. An AnalysisTemplate that checks SLO burn rate after promotion is a much better signal than any agent's opinion that "the change looks safe".

8. Autonomy levels per path, not per agent

Most autonomy discussions ask how much to trust an agent. I think that's the wrong unit. The same agent can be perfectly safe scaffolding a dev namespace and dangerous resizing a production database. Set autonomy per golden path, based on blast radius and reversibility, and let any agent use a path at that path's level.

The PlatformEngineering.org material describes four levels, moving from agents in the loop towards humans "on the loop" as orchestrators, with level 4 still a horizon. Here's how I'd map that onto concrete paths:

Golden pathBlast radiusStarting levelPromotion criteriaCeiling
Read catalog, docs, ownership, SLOsNoneAuto from day oneNone neededAuto
Scaffold a new service repoOne new repoRun, human merges30 runs, first-try gate pass above 90%, no revertsAuto-merge within policy
Non-prod infrastructure claimOne team namespace, capped costRun, human mergesSame, plus budget cap heldAuto-merge within policy
Dependency and base image upgradesMany services, reversibleRun, human mergesChange failure rate no worse than human upgradesAuto to non-prod, PR to prod
Production data store claim or resizeCustomer dataDraft onlyOnly after a quarter of clean draftsRun, two approvers
Promotion to productionCustomersPR via KargoVerification pass rate, SLO burn after promotionPR via Kargo
Delete anything statefulIrreversibleNot exposedn/aNot exposed

Store the level next to the path, in Git, and enforce it in the policy layer rather than in the agent's prompt. A prompt that says "always ask before merging" is a suggestion. A branch protection rule plus an OPA decision that says needs_approval is a control. I went into more detail on that split in the AI SRE autonomy ladder post.

9. Identity, tenancy and audit

On a multi-tenant platform, the agent's identity is as important as the human's. The OWASP Top 10 for Agentic Applications, published in December 2025, lists identity and privilege abuse (ASI03), agentic supply chain issues (ASI04) and cascading failures (ASI08) among its top risks. All three show up quickly on a platform where agents can create resources.

  • One identity per agent per team. An agent working for the payments team gets a token scoped to the payments namespace and the payments folder in the GitOps repo. It can't open merge requests against another team's folder, and CODEOWNERS makes sure the right humans review what it does.
  • Agents act on behalf of someone. Every merge request records both the agent identity and the human who started the session. When something goes wrong, "the agent did it" is not an acceptable answer for an incident review.
  • Read access is scoped too. Context matters for quality, but an agent working on one service doesn't need every team's runbooks and incident notes. Backstage actions run with the caller's credentials, so the permission policies you already have for the catalog can scope what an agent sees. Use them.
  • Trace every tool call. The Backstage MCP backend emits a span per tools/call and metrics for operation and session duration. Send them to the same observability stack your tenants already get by default, tagged with the agent identity.
  • Treat MCP servers as dependencies. Pin versions, review tool descriptions when they change, and don't let agents add new tool servers on their own. A changed tool description can steer an agent as effectively as a prompt injection.

10. How agentic platforms fail

Most of these failure modes aren't new. They're the classic platform problems, sped up by an actor that never gets tired and never asks a colleague.

Failure modeWhat it looks likeControl
Golden path bypassAgents write raw manifests because that's what they saw in training dataAdmission policy that only accepts platform XRs in tenant namespaces; make the path the easy route
Schema driftThe template, XRD and docs disagree; agents fail gates in loopsGenerate template parameters and docs from the XRD; one source of truth
Tool sprawlDozens of overlapping tools; agents pick the wrong oneSeparate MCP servers per domain, outcome-based names, an allowlist per agent
Rubber-stamp approvalsReviewers approve 40 agent MRs a day without reading themApproval only for high blast radius paths; show a rendered diff and policy result, not raw YAML
Cascading changesOne agent's upgrade triggers other agents to "fix" downstream breakageRate limits per path, change budgets per team per day, a kill switch that freezes all agent identities
Context poisoningOutdated or malicious text in docs or catalog descriptions steers the agentOwned, reviewed docs; treat catalog text as data; gates catch the result anyway
Cost runawayAgents create dev environments and never clean them upTTL labels on non-prod claims, quotas per namespace, cost reported per agent identity
Platform team as bottleneckEvery failed run escalates to the platform teamEscalate to the owning team, not the platform team; fix unclear contracts, not individual runs

One pattern from the Google SRE book's chapter on eliminating toil applies directly: automation should make the system more predictable, not just faster. If agents make your platform faster but your change failure rate goes up, you've automated toil into incidents. That's exactly the throughput-up, stability-down pattern DORA found.

11. Measuring it

You can't tell whether any of this works without splitting your delivery metrics by who made the change. Tag every merge request and promotion with the actor type (human, agent, or agent on behalf of a human) and the golden path it used. Then track:

  • DORA metrics by actor. Lead time, deployment frequency, change failure rate and recovery time, split by human and agent. Agent changes should not have a worse change failure rate than human ones on the same path.
  • First-try gate pass rate per path. This is the best single measure of contract quality. Low numbers mean the schema or docs are unclear, whoever is calling.
  • Retries and escalations per run. Rising retries after a platform release usually means you broke the contract without versioning it.
  • Golden path coverage. The share of infrastructure changes that went through a contract rather than raw YAML. This should rise as agent usage rises, not fall.
  • Review time on agent MRs. If it drops towards zero on high blast radius paths, you have rubber-stamping, not trust.

12. A 60-day plan and checklist

You don't need to rebuild your platform. Pick two or three paths and make them contracts.

WhenWhat
Week 1–2Inventory what agents already do in your repos. Pick the two most common changes (often a new service and a non-prod database). Write their contracts: schema, guarantees, owner.
Week 3–4Tighten the XRDs with enums, ranges, CEL rules and descriptions. Add the render, kubeconform and conftest gate in CI with JSON output. Run it on human MRs first.
Week 5–6Expose read-only catalog actions and the two templates over Backstage MCP actions, with a separate identity per agent. Turn on tracing for tool calls.
Week 7–8Let agents run the two paths at "run, human merges". Measure first-try pass rate, retries and escalations. Fix the contract wherever the agent fails, rather than adding to the prompt.
Day 60Review the numbers against the promotion criteria. Move one path up a level if it qualifies. Add the next path.

And the checklist I'd want ticked before any agent touches a golden path:

  1. Agents have no cloud credentials and no write kubeconfig. Git is the only write path.
  2. Each path has a versioned schema with types, enums, ranges and cross-field rules, enforced by the cluster, not just the form.
  3. Every field has a description a newcomer could act on.
  4. The same deterministic gates run for human and agent changes, on rendered output, with machine-readable errors.
  5. Retries are bounded, and failures escalate to the owning team with the draft and the gate output.
  6. Autonomy is set per path, stored in Git and enforced by policy and branch protection, not by the prompt.
  7. Production promotions go through Kargo with a pull request or approval step and post-promotion verification.
  8. Each agent has its own identity, scoped to one team, and every action records the human it acted for.
  9. Tool calls are traced into your normal observability stack and tagged by agent.
  10. MCP servers and tool definitions are pinned and reviewed like any other dependency.
  11. Sync windows and change freezes apply to agents automatically.
  12. There's a single switch that pauses every agent identity at once, and it's been tested.

The short version

Agents are about to become the busiest users of most internal platforms. That's good news if your golden paths are real contracts, because the platform becomes the place where you get consistency at a volume no review process can match. It's bad news if your paths are wizards with knowledge hidden in people's heads, because agents will route around them. So write the contracts down, put the rules in the schema, gate every probabilistic step with a deterministic one, keep Git as the only write path, and give each path the autonomy its blast radius deserves. Most of that is just good platform engineering, done strictly.

← All posts