Playbook: One Mandate, Two Systems
The scenario
A group buys through two systems. Plant 1000 raises purchase requisitions in SAP. The Munich plant raises requests in ServiceNow, where approving a requested item releases it for ordering. One procurement agent works both queues, under one procurement lead, with one budget.
The risk
Two systems usually mean two credentials and two permission models. The agent holds both credentials, and each system's rules are written in its own terms: a SAP release strategy here, a ServiceNow approval rule there. The two drift apart, and neither knows what the agent did in the other, so a daily budget is enforced in neither.
The mandate design
A mandate names the business action and the sites, never a tool or a vendor. Every tool of procurement.requisition.release reads the same facts under the same names, the plant and the amount among them, so one mandate governs both systems unchanged:
| Field | Value | Why |
|---|---|---|
| Action | procurement.requisition.release | Releasing in SAP and approving in ServiceNow are the same business action |
| Resources | procurement:plant:1000/requisition:* and procurement:plant:c7dae03a431069aecf016c0a0474d40a/requisition:* | Plant 1000 in SAP, and the Munich plant in ServiceNow |
max_amount | EUR 100,000 | One cap, in either system |
amount_above | EUR 10,000 | One approval threshold, in either system |
max_total | EUR 50,000 a day | One budget across both systems |
ServiceNow identifies a location by its sys_id, so the Munich plant is c7dae03a431069aecf016c0a0474d40a in every ServiceNow sandbox. Mandaitor reads the location from the requested item, and never goes by its name, which anyone allowed to rename a location could change.
Set it up
As a tenant administrator:
- Two sandboxes and their tools. On the Connectors page, create a SAP S/4HANA demo sandbox and a ServiceNow demo sandbox. Under Registered tools, enable
sap.purchase_requisition.releaseon the SAP sandbox's connector, andservicenow.requested_item.approveon the ServiceNow sandbox's. - One agent. Register it once, with the public half of a key only it holds.
- One mandate, bound to the agent:
const mandate = await adminClient.post("/mandates", {
principal: { type: "NATURAL_PERSON", subject_id: "cognito:a1b2c3d4-e5f6", 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:*", // SAP, plant 1000
"procurement:plant:c7dae03a431069aecf016c0a0474d40a/requisition:*", // ServiceNow, Munich plant
],
effect: "ALLOW",
},
constraints: {
transaction_limits: {
max_amount: { currency: "EUR", value: "100000.00" },
max_total: [{ amount: { currency: "EUR", value: "50000.00" }, window: "P1D" }],
},
escalation_rules: { amount_above: { currency: "EUR", value: "10000.00" } },
},
});
await adminClient.post(`/agents/${agent.agent_id}/bind`, { mandate_id: mandate.mandate_id });
Let the agent ask
tools/list now shows both tools. The agent names the record in each system's own terms:
// SAP: a purchase requisition, by its number.
await mcp.callTool({
name: "sap_purchase_requisition_release",
arguments: { purchase_requisition: "10000123" },
});
// ServiceNow: a requested item, by its number.
await mcp.callTool({
name: "servicenow_requested_item_approve",
arguments: { requested_item: "RITM0042002" },
});
Each call is decided against the same mandate, on facts read from its own system: the plant and the amount from the SAP requisition, the location and the price, with its own currency, from the ServiceNow item.
What must be refused
Run these against the two sandboxes' seeded records:
| Request | Expected | Why |
|---|---|---|
10000123: SAP, plant 1000, EUR 4,850.00 | SUCCEEDED | Within the mandate |
RITM0042002: ServiceNow, Munich plant, EUR 2,900.00 | SUCCEEDED | Within the same mandate |
RITM0042003: ServiceNow, Munich plant, EUR 12,000.00 | AWAITING_APPROVAL | Above the threshold, as in SAP |
RITM0042004: ServiceNow, Frankfurt office, EUR 480.00 | DENIED, NO_COVERING_MANDATE | A location the mandate does not name |
10000126: SAP, plant 2000, EUR 3,200.00 | DENIED, NO_COVERING_MANDATE | A plant the mandate does not name |
RITM0042002 again, once it is approved | DENIED, PRECONDITION_FAILED | Only an item waiting for approval is approved |
To see the shared budget, write the mandate with a max_total of EUR 5,000 a day instead. Release 10000123 (EUR 4,850.00) in SAP, then ask for RITM0042001 (EUR 349.00) in ServiceNow: it is DENIED with CUMULATIVE_LIMIT_EXCEEDED, because the mandate's day already holds EUR 4,850.00.
Mandaitor never approves over a person's decision in ServiceNow: an item a person rejected or cancelled is DENIED, with FACT_UNAVAILABLE. Reset demo data on the Connectors page restores either sandbox's seed.
Evidence and review
- The receipts say which system was written:
target.systemissap-s4hanaorservicenow, andtarget.recordthe requisition or the item. - The Executions page lists both systems' executions side by side, under the one mandate.
- The evidence pack holds one audit chain for the mandate, whichever system each write went to.
Adapt it
- More sites. Add each plant's pattern, in SAP's plant numbers or ServiceNow's location
sys_ids. - Different limits per site. Write one mandate per site; each counts its own limits.
- A test landscape. Register the agent once more in a test environment, with both tools enabled there on test connectors. See Environments.