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
| Field | Value | Why |
|---|---|---|
| Principal | The procurement lead, by their dashboard subject ID | A person stands behind the authority, and approves above the threshold |
| Delegate | The agent's DID | The one registered agent, which holds its own key |
| Action | procurement.requisition.release | The business action, never a SAP API |
| Resources | procurement:plant:1000/requisition:* | Plant 1000's requisitions, and nothing else |
max_amount | EUR 100,000 | A hard cap no approval overrides |
amount_above | EUR 10,000 | Above it, the procurement lead approves each release |
max_total, max_count | EUR 50,000 and 20 releases a day | Bounds what many small releases can add up to |
| Expiry | The end of the quarter | The authority lapses unless someone renews it |
Set it up
As a tenant administrator:
- The sandbox and its tool. On the dashboard's Connectors page, create a SAP S/4HANA demo sandbox, and enable
sap.purchase_requisition.releaseon its connector under Registered tools. - The agent. Register it on the Agent Registry page, with the public half of a P-256 key only it holds (how).
- 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:
| State | What the agent should do |
|---|---|
SUCCEEDED | Report the release, and keep the receipt with the case. |
AWAITING_APPROVAL | Tell the requester that the procurement lead decides, and check back later with get_execution. |
DENIED | Report the reason in state_reason, and stop. Never look for a way around a denial. |
FAILED | Report that the release did not happen. |
UNKNOWN | Do 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:
| Request | Expected | Why |
|---|---|---|
10000123: plant 1000, EUR 4,850.00 | SUCCEEDED | Within the mandate |
10000124: plant 1000, EUR 38,500.00 | AWAITING_APPROVAL | Above the approval threshold |
10000125: plant 1000, EUR 240,000.00 | DENIED, AMOUNT_ABOVE_LIMIT | Above the cap, which no approval overrides |
10000126: plant 2000, EUR 3,200.00 | DENIED, NO_COVERING_MANDATE | Another plant |
10000123 again, once it is released | DENIED, PRECONDITION_FAILED | A requisition is released once |
| Any request after the mandate is revoked | DENIED, NO_COVERING_MANDATE | No 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
qasenvironment, 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.