Agents in production

The proof of concept works.Production is where it stops.

You have the demo. You have the mandate. What you do not have is a way for the agent to touch the Kotlin service, the Java monolith and the Postgres of record — none of which anyone is going to rewrite, and at least one of which nobody still in the building fully understands.

That gap is not model quality. It is state, retries, permissions, approvals and audit — everything between a good answer and a write nobody has to apologise for. So this page is one flow, end to end, showing exactly where each of those lands.

The situation before

Four blockers, and none of them is the model.

This is the shape of the problem as platform, integration and architecture leads keep describing it to us. Recognise three of the four and the rest of this page is about you.

01

It can decide, so it must not act

An agent that both decides and acts is the thing nobody will sign off, and no amount of prompt engineering changes that. The fix has to be structural, not textual: separate the opinion from the authority to act on it.

02

The system of record speaks its own language

It is reached by code that already exists, in a runtime that is not the one your new work lives in. Porting it is a migration project that will not be funded this year, and probably not next.

03

Approvals were bolted on at the end

A person added to the loop late usually means a queue in a different tool, with the state of the work living in someone's head in between. That is where the audit trail quietly stops.

04

Nobody can reconstruct what happened

The first serious question after go-live is why a given record was decided the way it was, by whom, on what evidence. If that answer has to be assembled by hand, it will not be assembled.

The flow, step by step

Triage a record with an agent, then make a person authorise the write.

Five steps. Under each one is what it returns and how it is configured — the template's real inputs and defaults, not a diagram drawn for this page.

Read the badges. They mark where the agent reasons, where a person authorises, and where something outside Orvanta is called.

  1. step 1assessTypeScript stepThe agent reasons

    The model forms an opinion it has no authority to act on

    The record and your policy — plain language, a flow input, not code — go to any OpenAI-compatible endpoint. The reply must be JSON, and it is parsed and range-checked rather than trusted: prose fails here, a risk score of 900 fails here, loudly, before anything downstream sees it. A step you cannot branch on is a step you cannot govern.

    returns{ risk, rationale, recommended_action }timeout120sretry2 attempts, 5s apart
  2. step 2gateConditional branchOrvanta picks the route

    Interrupt a person only when their judgement is worth having

    The condition is the assessed risk against auto_approve_below. Above it, a human is asked. Below it, the agent's recommendation stands and is recorded as having been taken automatically — not quietly passed off as a decision someone made. Both paths return the same shape, so the write step never has to ask which one ran. Set the threshold to 0 and every record goes to a person.

    inputauto_approve_below (default 30)both paths return{ decision, note, decided_by }
  3. step 3authoriseSuspended stepA person authorises

    The run stops — for up to a day — and waits for a human

    It suspends. It is not polling and it is not holding a process open; the run is parked until an approver responds or the timeout expires. The step hands back resume, approve and cancel URLs, so how the person is reached is your choice — Slack, Teams, email, your own on-call tool. They see a small form: approve or reject, with an optional note.

    timeout86,400sresponses required1 — raise it for several approversformdecision: approve | reject · note
  4. step 4decideTypeScript stepOrvanta picks the route

    Read what the person actually chose, not what was proposed to them

    The approver's answer arrives as resume in the step directly after the suspended one — not as the suspended step's own result, which is only what it returned before it stopped. That distinction is easy to get backwards, and getting it backwards means someone clicks reject and watches the write go through anyway. This step exists so that cannot happen.

    readsresumerecordsdecided_by
  5. step 5applyTypeScript stepAn existing system is called

    One irreversible step, and it runs last

    The decision, the rationale, the approver and a timestamp are posted to your system of record. Everything before this point is reversible; this is the only step that changes anything outside Orvanta, and the only step that knows the downstream system exists. If yours does not speak HTTP, this is the one step you replace.

    inputstarget_url · auth_headerretry3 attempts, exponential from 2s

What you need before running it: an API key for any OpenAI-compatible endpoint, held as an Orvanta variable, and somewhere for the decision to land. Point that at a request bin first and watch the write arrive before you aim it at anything real.

Nothing gets rewritten

Two runtimes, one run, the same state.

The apply step above assumes HTTP. Most estates are not that tidy. The second reference flow is the honest version: the service you cannot touch is called by the code that already knows how to talk to it, and the record shape your systems expect is defined where your systems already define it.

A Python step reads the service. A TypeScript step reshapes the response into a typed record. A third appends the audit entry. Same flow, same run, state handed straight across the language boundary — no queue between them, no service to stand up, nothing ported.

Not HTTP at all? Replace one step. SOAP, a database, a file drop, an SFTP poll — nothing downstream changes, because nothing downstream knows.

Open this template
call-a-legacy-service-and-normalise-its-response
  • fetch_legacyPython

    Calls the existing service and returns its response untouched. Anything that is not JSON is passed on as text rather than crashing the run — old services are not always well behaved.

  • normaliseTypeScript

    Reshapes it into a typed record and fails loudly on anything unexpected. The field names are flow inputs, so the common case needs no code change at all. This is the step you are meant to edit.

  • record_auditTypeScript

    Appends the audit entry. Leave the audit URL empty and it reports that it did nothing, rather than pretending it succeeded.

When nobody is watching

The third flow runs on a schedule and has to earn the right to interrupt you.

Most operational review is a person reading a queue that is usually fine. Automate that naively — alert on everything — and the alerts are muted within a fortnight, taking the one that mattered with them.

So here the model's only authority is deciding whether to interrupt someone. It cannot act. Ambiguity escalates rather than resolving quietly, and the digest is posted on quiet days too — because a flow that has silently stopped running should not look like good news.

The escalation criteria are a flow input, written in plain language. Start strict; loosen it once the digest has earned some trust. The template ships with no schedule of its own, because the right cadence is yours to choose.

Open this template
Where this is worth the most

Regulated operations, with AI in the loop.

Finance, insurance and healthcare want models in back-office operations and cannot let them act unsupervised. That is not a different story from the one above — it is the same flow, in the place where the approval step stops being good practice and becomes the reason the project is allowed to exist at all.

The gate is the point, not a feature

In back-office operations the question is never whether the model is clever. It is who authorised the write, on what evidence, and whether that can be shown to someone who was not there.

Controls that already exist

Approval gates, granular role-based access control, single sign-on over SAML and OIDC, and a full audit log — the same mechanisms the flow above is built out of, rather than a compliance module bolted alongside it.

Somewhere you are allowed to run it

Self-hosted on Kubernetes, air-gapped, or EU-hosted managed cloud. The workspace is Git-backed, so every flow definition is a file you own and can diff.

The claim we are betting on

Weeks to production, without rewriting anything.

Not quarters, because there is no migration in front of you: the services you already run are called as steps, in the languages they are already written in. Not days either — you still have a policy to write, an approver to choose, and a threshold to tune until the escalation rate is one your team will actually tolerate.

The three flows on this page are real, not illustrations: each one has been run end to end against a working engine, and that first run is what caught the two defects that would otherwise have shipped in them. What we have not published is a measured time from empty workspace to first written decision — and we would rather say so than invent one. When it has been measured, the number and the conditions it was measured under will both appear here.

The page can't prove this.Your systems can.

The triage template is the whole argument as one runnable object: an agent that reasons, a person who authorises, and a single write to a system nobody had to rewrite. Open it in your own workspace, swap the stubbed call for your endpoint, and point it at the thing you were told could not be touched. That is the only test that counts.

Prefer to look around first? Browse every flow on the hub or see use cases by industry and capability.