Most "AI for ERP" pitches stop at retrieval: ask a natural-language question, get back a number pulled from the right tables. That is useful, and it is also the easy 20% of the problem. The harder, more valuable 80% starts when the copilot is allowed to write — adjust a reorder point, hold a shipment, escalate a reconciliation exception — because now a wrong answer is not a wrong sentence, it is a wrong transaction.

This is what we learned building autonomous copilots on top of live ERP systems for operations teams, and the four layers that had to exist before "autonomous" was a word we were comfortable using in front of a customer.

The gap between advice and action

A read-only copilot fails safely. It hallucinates a number, a human notices it looks wrong, nothing happens to the business. An action-taking copilot has no equivalent safety net unless you build one — by the time a human notices a bad reorder, stock is already on a truck.

So the first design decision is not a model choice, it is a taxonomy: which operations are advisory, which are reversible and low-blast-radius, and which are irreversible or customer-facing. We settled on three tiers:

Tier 1 — Advisory     : surface a recommendation, human decides   (no gate)
Tier 2 — Reversible   : execute, but log + allow one-click undo   (soft gate)
Tier 3 — Irreversible : requires explicit human approval          (hard gate)

A stock reorder under a pre-approved budget is Tier 2. Cancelling a customer's shipment is Tier 3, always, regardless of how confident the model is. Confidence scores are not a substitute for this taxonomy — a model can be very confident and still wrong about something expensive.

Scoping what a copilot can touch

The copilot does not get a service account with the same privileges as the ERP's API. It gets a narrow set of typed actions, each with its own validation, independent of whatever the underlying ERP API would technically allow:

type CopilotAction =
  | { kind: 'create_purchase_order'; sku: string; qty: number; maxUnitCost: number }
  | { kind: 'hold_shipment'; shipmentId: string; reason: string }
  | { kind: 'flag_invoice_mismatch'; invoiceId: string; delta: number };

function validate(action: CopilotAction, tenant: Tenant): Result {
  if (action.kind === 'create_purchase_order') {
    if (action.qty > tenant.policy.maxAutoReorderQty) return deny('qty over policy limit');
    if (action.maxUnitCost > tenant.policy.maxUnitCost) return deny('cost over policy limit');
  }
  return allow();
}

The model never emits a raw database write or a free-form API call. It emits one of these typed actions, and the action is what gets validated, logged, and — depending on tier — gated. This also means a prompt injection embedded in, say, a supplier email the copilot summarised cannot do anything worse than propose an action that then fails validation.

The approval layer

For Tier 3 actions, the copilot's output is a proposal, not an execution, and the proposal has to carry enough context that approving it is a real decision rather than a rubber stamp:

  • What the action is, in plain terms, with the exact parameters
  • Why the model proposed it — the specific data it reasoned from
  • What happens if nobody approves it within the SLA
  • A diff against the current state, not just the proposed end state

We learned this the slow way: an early version surfaced approvals as "Approve reorder for SKU-4471?" with no context, and approvers started approving everything within seconds — which defeats the point of the gate. Once we required the reasoning and the diff to be visible inline, approval time went up slightly and the reject rate went up meaningfully, which is the gate actually working.

A gate nobody reads is not a gate If approvers are rubber-stamping, the interface is failing, not the process. Measure reject rate on Tier 3 actions — if it's near zero, the context you're showing isn't doing its job.

Reasoning over live, changing state

ERP data is not a static document to retrieve once and answer from — inventory counts, order statuses, and prices change while the copilot is mid-reasoning. A multi-step plan built on a snapshot from thirty seconds ago can propose an action against state that no longer exists.

const plan = await model.plan(query, { snapshotAt: now() });

// Re-validate against current state immediately before execution,
// not just at planning time
const current = await erp.read(plan.affectedEntities);
if (hasChanged(plan.snapshot, current)) {
  return { status: 'stale', action: 'replan' };
}
await execute(plan.action);

This re-check is cheap relative to the cost of executing against stale state, and it catches the specific class of bug where the plan was correct when formed and wrong by the time it runs — common in anything touching inventory during a busy shift.

Audit trails as a first-class output

Every action the copilot proposes or executes writes a structured record — not a log line for debugging, but a first-class output the business can query: who (or what) initiated it, what data it reasoned over, who approved it, and what it changed. This is what lets an operations lead answer "why did we reorder 400 units of this last Tuesday" six weeks later without reconstructing it from memory.

{
  "actionId": "act_8f21",
  "kind": "create_purchase_order",
  "proposedBy": "copilot:reorder-agent@v3",
  "reasoning": "SKU-4471 projected stockout in 6d at current velocity",
  "inputSnapshot": { "onHand": 42, "avgDailyDemand": 11.2, "leadTimeDays": 9 },
  "tier": 2,
  "approvedBy": null,
  "executedAt": "2026-05-14T09:12:03Z",
  "undoneAt": null
}

Treat this the same way you'd treat financial audit logs: append-only, tenant-scoped, and retained on a schedule that matches compliance requirements — not an afterthought bolted on once someone asks for it during a security review.

Rolling it out without an incident

We shipped autonomy in stages per tenant, not per feature:

Stage 1 — Shadow mode : copilot proposes, logs, never executes     (weeks)
Stage 2 — Tier 2 live : reversible actions execute automatically   (weeks)
Stage 3 — Tier 3 live : irreversible actions available, gated      (ongoing)

Shadow mode is the part teams are tempted to skip because it produces no visible feature. It's also what tells you, before anything executes, what fraction of proposals a human would have rejected — the single best predictor of whether Stage 2 is ready. We hold at Stage 1 until shadow-mode agreement is above 95% for two consecutive weeks before enabling any live execution.

What changed in practice

MetricBefore copilotAfter (Tier 1–2 live)
Time to flag a stockout risk~18 h (batch report)~4 min
Reorder actions requiring manual entry100%31%
Tier 3 approval reject rate14%
Stale-state re-plans caught pre-execution~6% of plans

None of these numbers come from a bigger or smarter model. They come from the tiering, the typed action surface, and the shadow rollout — the parts that make "autonomous" mean "trusted with real operations" rather than "unsupervised."

Talk to our team