If you're running Adobe Commerce in a headless setup — PWA Studio, AEM, or a custom storefront on GraphQL. you're already halfway to agentic commerce without realizing it. The headless architecture is the foundation: your storefront is just one of many possible "clients" calling Adobe Commerce's GraphQL API. An AI agent is just another client. The question is how you let that client act safely.
This post walks through how the 5-agent governance pattern maps onto Adobe Commerce's actual APIs, modules, and extension points with concrete examples using GraphQL mutations, Adobe I/O Events, and the Commerce Admin.
Why headless Adobe Commerce is the right starting point
In a monolithic (non-headless) Commerce setup, "the storefront" and "the business logic" are tangled together Knockout.js templates calling PHP controllers directly. Bolting an AI agent onto that means the agent has to behave like a browser: navigate pages, click buttons, parse HTML.
In headless Adobe Commerce, none of that exists. Everything catalog search, cart creation, pricing, checkout, order placement is already exposed as discrete GraphQL operations:
query { products(search: "ergonomic keyboard") { items { sku name price_range { ... } } } }
mutation { createEmptyCart }
mutation { addProductsToCart(cartId: $cartId, cartItems: [...]) }
mutation { setShippingAddressesOnCart(...) }
mutation { setPaymentMethodOnCart(...) }
mutation { placeOrder(input: { cart_id: $cartId }) }
This is exactly the shape an agent needs: clean, typed, callable operations rather than a UI to scrape. The work isn't "teach the agent to use Commerce" it's deciding which of these operations each agent is allowed to call, and under what conditions.
That's the governance layer this post adds.
Mapping the 5 agents onto Adobe Commerce
The key architectural point: the policy engine and audit log live outside Commerce, as a separate microservice. Commerce stays the system of record for catalog, pricing, and orders, it doesn't need to "know" about agents at all. The agent layer is a new client sitting in front of your existing GraphQL endpoint, exactly like your PWA storefront does.
1. Intent Understanding Agent mapping to Commerce's data model
The agent turns natural language into a structured spec, but the fields of that spec should map directly to things Commerce already understands store views, customer groups, attribute sets:
from pydantic import BaseModel, Field
class ProcurementGoalSpec(BaseModel):
product_search_query: str
quantity: int = Field(gt=0)
max_unit_price: float
total_budget: float
store_view_code: str # e.g. "b2b_us_en" - which storefront context
customer_group_id: int # determines pricing/catalog visibility
company_id: Optional[str] # for B2B Commerce / company accounts
delivery_deadline_days: int
preference_weights: dict[str, float]
requested_by_email: str
The store_view_code and customer_group_id matter a lot in Adobe Commerce they determine which catalog price rules, customer-group-specific pricing, and shared catalogs (if you're on B2B Commerce) apply. An agent acting on behalf of a B2B company account needs to inherit that company's negotiated pricing and catalog restrictions automatically, it shouldn't see prices or products outside what that company's buyers would see.
2. Product Discovery Agent Catalog Service / Live Search, read-only
This agent's tools are GraphQL queries only never mutations. If you're on Adobe Commerce with Catalog Service and Live Search (the API Mesh–backed services), this is even cleaner because search relevance, facets, and merchandising rules are already handled server-side.
import requests
CATALOG_SERVICE_ENDPOINT = "https://catalog-service.adobe.io/graphql"
def search_catalog(query: str, store_view: str, customer_group: int):
graphql_query = """
query SearchProducts($phrase: String!) {
productSearch(phrase: $phrase) {
items {
productView {
sku
name
inStock
attributes(roles: ["visible_in_pdp"]) { name value }
}
}
}
}
"""
headers = {
"Magento-Environment-Id": MAGENTO_ENV_ID,
"Magento-Store-View-Code": store_view,
"Magento-Customer-Group": str(customer_group),
"Magento-Website-Code": "base",
"x-api-key": LIVE_SEARCH_API_KEY,
}
return requests.post(CATALOG_SERVICE_ENDPOINT,
json={"query": graphql_query, "variables": {"phrase": query}},
headers=headers).json()
Notice the headers Magento-Customer-Group and Magento-Store-View-Code are what make Live Search return results that respect that customer's actual pricing and catalog visibility. This is the first enforcement layer, and it's free you get it just by using Commerce's existing context headers correctly. An agent acting for a B2B buyer literally cannot "see" products outside their assigned shared catalog, because Live Search won't return them.
For availability, query MSI (Multi-Source Inventory) stock status per source important if you're fulfilling from multiple warehouses:
query CheckStock($sku: String!) {
products(filter: { sku: { eq: $sku } }) {
items {
sku
stock_status
only_x_left_in_stock
}
}
}
3. Pricing Optimization Agent - Pricing Service + B2B price lists
This is where Adobe Commerce's pricing complexity becomes an asset, not a problem you already have rich, structured pricing data to optimize against, instead of having to infer it.
The four scoring factors map to Commerce concepts:
| Generic factor | Adobe Commerce equivalent |
|---|---|
| Price |
price_range.minimum_price.final_price (already includes catalog price rules, tier pricing, customer-group pricing) |
| Preference match | Product attributes + Commerce Merchandising Services relevance score |
| Vendor trust | For marketplace/multi-vendor setups: seller rating attribute; otherwise: supplier metadata in a custom attribute set |
| Availability | MSI stock per source + delivery_date from shipping method estimates |
def utility_score(product, weights, pref_vector, customer_group):
# final_price already reflects this customer's negotiated/tier pricing
price = product["price_range"]["minimum_price"]["final_price"]["value"]
f_price = 1 - normalize(price, category_price_min, category_price_max)
f_pref = cosine_similarity(embed(product["name"] + " " + product["description"]), pref_vector)
# Custom attribute set on supplier/vendor, e.g. 'vendor_reliability_score'
f_trust = product.get("vendor_reliability_score", 0.5)
# From MSI: does this source have stock + can it ship within deadline?
f_avail = estimate_delivery_probability(product["sku"], deadline_days=5)
return (weights["price"] * f_price + weights["pref"] * f_pref +
weights["trust"] * f_trust + weights["avail"] * f_avail)
For B2B Commerce specifically: if the company has a negotiable quote workflow enabled, the Pricing Agent should check whether the requested quantity qualifies for quote-based pricing rather than catalog pricing and if so, route toward creating a Requisition List or Negotiable Quote instead of a direct order. That's a natural "escalation" path that B2B Commerce already supports natively:
mutation CreateRequisitionList($name: String!) {
createRequisitionList(input: { name: $name, description: "AI-generated procurement list" }) {
requisition_list { uid name }
}
}
This gives you a built-in "draft" state for high-value AI-generated orders a human buyer reviews the requisition list before it becomes a real quote/order, using UI your B2B buyers already know.
4. Risk & Trust Evaluation Agent — using Commerce's own customer data
Adobe Commerce already stores the historical data this agent needs, you're not building a new data source, you're querying what's there:
def compute_risk_score(order_spec, customer_id, company_id=None):
# Query Commerce Admin REST API for order history
order_history = get_customer_order_history(customer_id, months=12)
avg_order_value = mean([o["grand_total"] for o in order_history])
v_norm = order_spec.total_budget / (avg_order_value * 5) # vs. 5x typical order
# Is this SKU/category new for this customer?
purchased_categories = {cat for o in order_history for cat in o["categories"]}
category_novelty = 0.0 if order_spec.category in purchased_categories else 1.0
# B2B: check company credit limit (if using Adobe Commerce B2B's Company Credit)
if company_id:
credit = get_company_credit_balance(company_id)
credit_risk = order_spec.total_budget / credit["available_balance"]
else:
credit_risk = 0.0
risk_score = 0.35 * v_norm + 0.25 * category_novelty + 0.40 * credit_risk
return {
"risk_score": risk_score,
"company_credit_available": credit.get("available_balance") if company_id else None,
"route": route_from_score(risk_score),
}
If you're on B2B Commerce with Company Credit enabled, this is especially clean, Commerce already tracks credit_limit and available_balance per company. The Risk Agent just reads it. An AI agent proposing a $40,000 purchase against a company with $35,000 available credit is an automatic, obvious escalation using data Commerce was already maintaining for entirely different reasons.
5. The policy engine — OPA sitting in front of your GraphQL mutations
This is the piece that doesn't exist in Commerce today, and shouldn't live inside Commerce, it should sit as a thin proxy/middleware in your headless gateway (API Mesh, or a Node/Express layer in front of GraphQL).
package agentic_commerce.policy
import future.keywords.if
import future.keywords.in
default allow = false
default escalate = false
# Auto-approve: small order, existing customer, category they've bought before
allow if {
input.action_type == "PLACE_ORDER"
input.order.grand_total <= data.limits.auto_approve_threshold
input.customer.category_novelty == 0
input.risk.credit_risk < 0.5
}
# Escalate to Requisition List (B2B): mid-value or new category
escalate if {
input.order.grand_total > data.limits.auto_approve_threshold
input.order.grand_total <= data.limits.requisition_threshold
}
# Escalate: would exceed 80% of available company credit
escalate if {
input.company_id != null
input.order.grand_total > (input.risk.company_credit_available * 0.8)
}
The middleware sits between the agent and Commerce's GraphQL endpoint:
// API Mesh resolver or Express middleware in front of GraphQL
app.post('/agent/graphql', async (req, res) => {
const { query, variables } = req.body;
// Only intercept mutations that place orders
if (isPlaceOrderMutation(query)) {
const policyInput = buildPolicyInput(variables, req.agentContext);
const decision = await evaluatePolicy(policyInput); // call to OPA
if (decision.result === "DENY") {
return res.status(403).json({ errors: [{ message: "Policy denied transaction" }] });
}
if (decision.result === "ESCALATE") {
// Redirect to Requisition List creation instead of placeOrder
const reqListResult = await createRequisitionList(variables, req.agentContext);
await logAuditEvent({ action: "ESCALATED", target: "requisition_list", ...reqListResult });
return res.json({ data: { escalated: true, requisitionList: reqListResult } });
}
}
// PERMIT - forward to actual Commerce GraphQL endpoint
const result = await forwardToCommerceGraphQL(query, variables, req.agentContext);
await logAuditEvent({ action: "EXECUTED", query, result });
return res.json(result);
});
This pattern a policy-aware proxy in front of GraphQL is something API Mesh is actually well-suited for. You can implement it as a Mesh resolver that wraps the placeOrder mutation specifically, without touching Commerce core at all.
Execution Agent — the actual cart-to-order flow
Once PERMIT is granted, the Execution Agent runs the standard headless checkout sequence, the exact same GraphQL calls your PWA storefront uses, just driven programmatically:
def execute_purchase(order_spec, approved_items, customer_token):
headers = {"Authorization": f"Bearer {customer_token}",
"Store": order_spec.store_view_code}
# 1. Create cart (or use existing customer cart)
cart_id = graphql_call("mutation { createEmptyCart }", headers=headers)
# 2. Add items
graphql_call("""
mutation AddItems($cartId: String!, $items: [CartItemInput!]!) {
addProductsToCart(cartId: $cartId, cartItems: $items) {
cart { items { quantity product { sku } } }
}
}
""", variables={"cartId": cart_id, "items": approved_items}, headers=headers)
# 3. Set shipping (from company default address for B2B)
graphql_call("mutation SetShipping(...) { ... }", headers=headers)
# 4. Set payment method — IMPORTANT: see note below
graphql_call("mutation SetPayment(...) { ... }", headers=headers)
# 5. Place order
result = graphql_call("""
mutation PlaceOrder($cartId: String!) {
placeOrder(input: { cart_id: $cartId }) {
order { order_number }
}
}
""", variables={"cartId": cart_id}, headers=headers)
return result
A critical Adobe Commerce-specific note on payment: never give the agent a generic "credit card" payment method with stored card details it can freely use. Instead, use Purchase Order payment method (built into B2B Commerce) or a pre-authorized stored payment profile tied to the company account with Commerce's own spending limits already configured. The agent should be restricted, at the Commerce payment method level, to payment methods that are themselves governed, so even if your external policy engine had a bug, Commerce's own payment configuration is a second backstop.
Audit trail — using Adobe I/O Events
Adobe Commerce already has an eventing system built for exactly this kind of "record everything that happens" requirement, Adobe I/O Events for Adobe Commerce. Instead of building a custom logging pipeline from scratch, subscribe to (and extend) Commerce's native order events:
// app/code/YourCompany/AgenticAudit/etc/events.xml
<event name="sales_order_place_after">
<observer name="agentic_audit_log"
instance="YourCompany\AgenticAudit\Observer\LogAgentDecision" />
</event>
// Observer adds the agent's reasoning trace as order metadata
class LogAgentDecision implements ObserverInterface
{
public function execute(Observer $observer)
{
$order = $observer->getEvent()->getOrder();
// Custom order attribute storing the full reasoning trace as JSON
$order->setData('agent_reasoning_trace', json_encode([
'requested_by' => $order->getCustomerEmail(),
'agent_pipeline_run_id' => $this->currentRunId,
'candidates_considered' => $this->candidatesEvaluated,
'selection_rationale' => $this->utilityScores,
'risk_assessment' => $this->riskRecord,
'policy_decision' => $this->policyOutcome,
]));
$order->save();
}
}
This makes the reasoning trace visible directly in the Commerce Admin order view a custom tab on the order detail page showing "Why the AI chose this vendor / these items," which is exactly what a merchant ops team needs when investigating an order. You get the audit trail and the UI to view it, using infrastructure Commerce already ships with.
For the tamper-evident hash-chaining piece, push the same event to an external audit service via Adobe I/O Events webhooks Commerce emits the event, your external service hash-chains and stores it, Commerce doesn't need to know anything about hashing.
Human-in-the-loop — using the Admin Grid you already have
When the policy engine escalates, the natural Adobe Commerce home for "things waiting for human approval" is either:
- Requisition Lists (if B2B Commerce) buyers/approvers already have a UI for this
-
A custom Admin grid a UI extension showing pending agent-generated orders with an Approve/Reject action, built using standard Magento UI Components (
ui_componentXML + grid listing)
<!-- view/adminhtml/ui_component/agent_pending_orders_listing.xml -->
<listing>
<dataSource name="agent_pending_orders_listing_data_source">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="provider" xsi:type="string">agent_pending_orders</item>
</item>
</argument>
</dataSource>
<columns>
<column name="customer_email" />
<column name="grand_total" />
<column name="risk_score" />
<column name="selection_rationale" component="Vendor_Module/js/grid/expandable-text" />
<actionsColumn name="actions">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="urlEntityParamName">order_id</item>
</item>
</argument>
</actionsColumn>
</columns>
</listing>
Approving in this grid simply triggers the placeOrder mutation that was held back, the merchant admin doesn't need new tools, just a new grid inside the Admin panel they already live in every day.
Putting it together: the headless-specific summary
| Generic concept | Adobe Commerce headless implementation |
|---|---|
| Structured order spec | Maps to store view, customer group, company ID fields |
| Read-only product search | Catalog Service / Live Search with customer-group headers |
| Multi-objective pricing | Pricing Service final_price + custom vendor attributes + MSI availability |
| Risk evaluation | Customer order history (REST Admin API) + B2B Company Credit balance |
| Policy engine | OPA as middleware/API Mesh resolver in front of placeOrder mutation |
| Escalation path | B2B Requisition Lists (native) or custom Admin grid |
| Execution | Standard GraphQL checkout mutations, restricted payment methods (PO payment) |
| Audit trail | Custom order attribute + Adobe I/O Events → external hash-chained log |
The big advantage of doing this on Adobe Commerce headless specifically: most of the "hard parts" already exist customer-group pricing, MSI availability, B2B credit limits, requisition list approval flows, Admin UI components, and an eventing system. The governance layer described in the original architecture isn't something you're building from scratch on top of Commerce — it's mostly wiring together capabilities Commerce already has, plus one new piece (the OPA policy proxy) sitting in front of your GraphQL gateway.
Where to start if you're prototyping this
If you want to try a minimal version this week:
- Stand up OPA locally, write one simple rule (
auto_approve if grand_total < 500) - Add a thin Express/Node proxy in front of your existing Commerce GraphQL endpoint that calls OPA before forwarding
placeOrder - Add one custom order attribute (
agent_reasoning_trace) and populate it via ansales_order_place_afterobserver - For escalations, just create a Requisition List instead of placing the order directly — B2B Commerce gives you the approval UI for free
That's enough to demonstrate the full loop agent decides, policy checks, order either places or escalates, and the Admin team can see exactly why without touching Commerce core at all.
Running agentic commerce on Adobe Commerce, Adobe Experience Manager, or another headless platform? I'd love to hear what extension points you've found useful drop a comment below.
Tags: #adobecommerce #magento #headless #ai

Top comments (0)