Skip to main content

Playbook: Requisition Release Agent

The scenario​

A procurement agent works the requisition queue of plant 1000. When a requisition is complete and within budget, the agent releases it in SAP, so that purchasing can order. Small releases should happen without anyone waiting. Larger ones need the procurement lead. Very large ones should never be released by an agent at all.

The risk​

Give the agent a SAP user, and it can release any requisition that user can: at any plant, of any amount, as often as it likes. A requisition text with an injected instruction, a confused plan or a bug could release the wrong requisition, or a large sum in many small steps.

Checking with an authorization service before each release does not close that. The agent still holds the SAP credential, it states the amount that is checked, and nothing proves that it released what was checked.

The mandate design​

FieldValueWhy
PrincipalThe procurement lead, by their dashboard subject IDA person stands behind the authority, and approves above the threshold
DelegateThe agent's DIDThe one registered agent, which holds its own key
Actionprocurement.requisition.releaseThe business action, never a SAP API
Resourcesprocurement:plant:1000/requisition:*Plant 1000's requisitions, and nothing else
max_amountEUR 100,000A hard cap no approval overrides
amount_aboveEUR 10,000Above it, the procurement lead approves each release
max_total, max_countEUR 50,000 and 20 releases a dayBounds what many small releases can add up to
ExpiryThe end of the quarterThe authority lapses unless someone renews it

Set it up​

As a tenant administrator:

  1. The sandbox and its tool. On the dashboard's Connectors page, create a SAP S/4HANA demo sandbox, and enable sap.purchase_requisition.release on its connector under Registered tools.
  2. The agent. Register it on the Agent Registry page, with the public half of a P-256 key only it holds (how).
  3. The mandate. Create it with Create mandate on the Governance page, and bind it to the agent. Through the API, from your own session:
const mandate = await adminClient.post("/mandates", {
principal: {
type: "NATURAL_PERSON",
subject_id: "cognito:a1b2c3d4-e5f6", // the procurement lead
display_name: "Procurement lead",
},
delegate: { type: "AGENT", subject_id: agent.did, display_name: "Procurement agent" },
scope: {
actions: ["procurement.requisition.release"],
resources: ["procurement:plant:1000/requisition:*"],
effect: "ALLOW",
},
constraints: {
time: { expires_at: "2026-12-31T23:59:59Z" },
transaction_limits: {
max_amount: { currency: "EUR", value: "100000.00" },
max_total: [{ amount: { currency: "EUR", value: "50000.00" }, window: "P1D" }],
},
rate_limits: { max_count: [{ count: 20, window: "P1D" }] },
escalation_rules: { amount_above: { currency: "EUR", value: "10000.00" } },
},
});
await adminClient.post(`/agents/${agent.agent_id}/bind`, { mandate_id: mandate.mandate_id });

A mandate with any constraint an execution does not read never backs one, so keep to the keys in A mandate written for executions.

Let the agent ask​

The agent names the requisition, and nothing else. It never states an amount or a plant: Mandaitor reads both from SAP. Over MCP, connected as in Getting Started:

const result = await mcp.callTool({
name: "sap_purchase_requisition_release",
arguments: {
purchase_requisition: "10000124",
purpose: "Retrofit approved in the Q4 capex review", // shown to the approver, never decides
},
});
const execution = result.structuredContent as { execution_id: string; state: string; state_reason: string };

Without MCP, the SDK's executions client asks the same way (@mandaitor/sdk 5.1.0 or later):

const { execution: requested } = await executions.requestExecution({
tool: "sap.purchase_requisition.release",
arguments: { purchase_requisition: "10000124" },
idempotency_key: crypto.randomUUID(),
purpose: "Retrofit approved in the Q4 capex review",
});
const execution = await executions.waitForExecution(requested.execution_id);

What the agent does with each answer:

StateWhat the agent should do
SUCCEEDEDReport the release, and keep the receipt with the case.
AWAITING_APPROVALTell the requester that the procurement lead decides, and check back later with get_execution.
DENIEDReport the reason in state_reason, and stop. Never look for a way around a denial.
FAILEDReport that the release did not happen.
UNKNOWNDo not ask again. Mandaitor reads the record back until it knows, and a person settles what reading cannot.

What must be refused​

Run these against the sandbox's seeded requisitions:

RequestExpectedWhy
10000123: plant 1000, EUR 4,850.00SUCCEEDEDWithin the mandate
10000124: plant 1000, EUR 38,500.00AWAITING_APPROVALAbove the approval threshold
10000125: plant 1000, EUR 240,000.00DENIED, AMOUNT_ABOVE_LIMITAbove the cap, which no approval overrides
10000126: plant 2000, EUR 3,200.00DENIED, NO_COVERING_MANDATEAnother plant
10000123 again, once it is releasedDENIED, PRECONDITION_FAILEDA requisition is released once
Any request after the mandate is revokedDENIED, NO_COVERING_MANDATENo active mandate covers it

Over MCP, a repeat of the same call within 10 minutes of its execution's end answers with that execution; ask again after that, or through the execution API with a new idempotency key. To test the daily limits, write the mandate with a max_count of 1 a day instead: once 10000123 is released, any other release that day is DENIED with CUMULATIVE_LIMIT_EXCEEDED, before anything is written.

Two seeded requisitions test the agent's handling of faults at the target. 10000127 is released, but SAP's answer times out: the execution is UNKNOWN until Mandaitor reads it back, then SUCCEEDED. 10000128 answers 503 and changes nothing: FAILED. Either way, the agent must not ask twice while the outcome is open. Reset demo data on the Connectors page restores every seeded requisition.

Evidence and review​

  • The receipt of each release names the requisition, its version before and after, the approval if there was one, and the Proof-of-Mandate the write was made on. The agent has it in the execution; keep it with the case.
  • The approvals are bound to a digest of the requisition the procurement lead saw. One given for a record that has since changed does not count.
  • The Executions page shows every request, the denials included, with the facts read from the record and the reason for its outcome.
  • The evidence pack of the mandate exports its audit chain, with every Proof-of-Mandate and receipt in proof_tokens. It answers an auditor's question: who released this requisition, and on whose authority?

Adapt it​

  • More plants. List each plant's pattern in resources. A * never crosses from one plant to another.
  • Another approver. Name them in escalation_rules.escalate_to, by their canonical subject ID.
  • MFA on every approval. Set require_mfa: true.
  • Test before production. Register a second agent in a qas environment, and enable the tool there on a connector to your test system. An agent registered for test never reaches production. See Environments.
  • Another vendor. The same mandate governs the release in ServiceNow: see One mandate, two systems.