The working journal / Architecture / Client experience
Many Supplier Portals. One Purchasing Workflow.
How I proposed combining ODC, an LLM, and a Playwright service for enterprise purchasing, explained through an illustrative auto-parts order.
Placing one supply order can mean visiting ten websites: signing in, finding parts, checking stock, and assembling carts. A shortage sends someone back through the process with another supplier.
I worked on an enterprise architecture proposal for this problem that reached a working prototype. The design used an LLM to understand supplier websites and help build reusable instructions. An agent could then call those automations through controlled tools.
I have kept the client anonymous and used an illustrative auto-service business here. NAPA Auto Parts and Advance Auto Parts are example suppliers, with no client relationship, partnership, or validated integration implied. Product references are real; carts, inventory, prices, account screens, and selectors are synthetic. No live purchases were made for this article.
The parts request
Take a multi-location auto-service business replenishing 24 spark plugs, 12 oil filters, and eight wiper blades. Its internal parts master already identifies the approved items and destination. The model is sourcing those exact parts.
The adapter needs both manufacturer part numbers and supplier SKUs, plus units, pack sizes, and the account or store behind each quote. Ordering 24 packs instead of 24 individual plugs would be an expensive mapping error.
Illustrative request / DEMO-REQ-1042
| Part | Identifiers | Published dimensions | Request |
|---|---|---|---|
| NGK V-Power spark plug | BKR6E-11 Catalog no. 2756 | M14 x 1.25 thread; 19 mm reach Product reference | 24 each |
| FRAM Extra Guard oil filter | PH3614 Catalog no. PH3614 | 3.430 in height; 2.984 in outside diameter; 3/4-16 thread Product reference | 12 each |
| Bosch XpressFIT PRO wiper blade | 24FP Catalog no. 24FP | 24 in length; attachment and installation position require fitment verification Product reference | 8 each |
Dimensions checked against the linked references on September 25, 2026. Drawings are generic schematics.
These parts replenish mixed stock. Dimensions alone do not establish vehicle compatibility; vehicle-specific orders need approved fitment evidence for the engine, year, configuration, and installation position.
Documentation: NAPA: NGK 2756 / BKR6E-11; FRAM: PH3614 product specifications; Bosch: XpressFIT PRO part lengths
Preparing a supplier workflow
Before the first order, the LLM can inspect permitted, sanitized supplier HTML and propose selectors for login fields, search controls, product rows, quantities, and cart actions. An engineer reviews the instructions, tests exact-part matches, shortages, and expired sessions, then releases a versioned supplier adapter.
The ordering agent calls that tested adapter through operations such as search_inventory, prepare_cart, read_cart, and submit_approved_order, each with constrained inputs and structured outputs. The browser script is reused across purchases. Each supplier still needs account setup, maintained mappings, tests, and an owner. I would use a suitable authorized API wherever one is available.
Supplier HTML is untrusted input. Product-page text must never change approval rules, request secrets, or authorize generated code. When a site changes, the affected adapter goes back through validation before it is used for purchasing again.
Login, search, and cart steps
The mock HTML below makes each XPath target visible. For a maintained adapter, I would prefer meaningful labels, roles, and stable attributes. Long positional paths break easily when a supplier changes its layout.
Playwright supports XPath, but it does not cross shadow roots. An iframe login needs the correct frame context. Every locator should identify one intended target, and the worker needs to read back the result of each action.
Inspect the instructions / mock HTML
Open a step to see its target highlighted, or try the sequence in the linked local demo.
01Locate the login fields
Supplier account
Sign in
Display only. The highlighted input is the XPath target.
XPath target
//*[@id='mock-login']//input[@name='email']The selector targets this login form. The worker fills credentials locally and returns the login status to the agent.
TypeScript / Playwright
await page.locator("xpath=//*[@id='mock-login']//input[@name='email']").fill(demoEmail);
await page.getByLabel('Demo passphrase').fill(demoPassphrase);
await page.getByRole('button', { name: 'Sign in to demo', exact: true }).click();
await page.locator('#demo-catalog').waitFor({ state: 'visible' });02Search the exact part number
Signed-in state / simulated
Exact match: NGK BKR6E-11
Displayed stock: 16 each
Observed at: example lookup time
XPath target
//*[@id='mock-search']//input[@name='partNumber']Look up SKU 2756, then confirm manufacturer part BKR6E-11 before using the displayed stock.
TypeScript / Playwright
await page.locator("xpath=//*[@id='mock-search']//input[@name='partNumber']").fill('2756');
await page.getByRole('button', { name: 'Search demo parts', exact: true }).click();
const product = page.locator("[data-mpn='BKR6E-11']");
await product.waitFor({ state: 'visible' });03Prepare 16 units, then read the cart
NGK V-Power
BKR6E-11 / SKU 2756
$3.20 each / mock price
Expected cart: BKR6E-11 x 16 = $51.20 merchandise. Checkout is a separate authorized action.
XPath target
//*[@data-mpn='BKR6E-11']//button[@data-action='add-to-cart']This mock supplier has 16 available. Add that allocation, then read back the cart line to confirm the part and quantity.
TypeScript / Playwright
await product.getByLabel('Quantity').fill('16');
await page.locator("xpath=//*[@data-mpn='BKR6E-11']//button[@data-action='add-to-cart']").click();
await page.locator('#demo-cart').waitFor({ state: 'visible' });
// Read the exact part and quantity back. Do not submit an order here.Open the isolated mock portal / Download the TypeScript example
The download runs only against a loopback URL. It uses dummy credentials and mock selectors to prepare a cart, with no secret-store connection or order submission.
Read the complete local-demo example
import type { Page } from '@playwright/test';
// Teaching fixture only. No real retailer, secret store, or purchase endpoint.
// Run against the local website preview, e.g. http://127.0.0.1:4333.
export async function prepareDemoCart(page: Page, baseUrl: string) {
const base = new URL(baseUrl);
if (base.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname)) {
throw new Error('This example only operates on a local mock portal.');
}
await page.goto(new URL('/resources/supplier-portal-demo.html', base).href);
await page.locator("xpath=//*[@id='mock-login']//input[@name='email']")
.fill('demo@example.invalid');
await page.getByLabel('Demo passphrase').fill('not-a-secret');
await page.getByRole('button', { name: 'Sign in to demo', exact: true }).click();
await page.locator('#demo-catalog').waitFor({ state: 'visible' });
await page.locator("xpath=//*[@id='mock-search']//input[@name='partNumber']")
.fill('2756');
await page.getByRole('button', { name: 'Search demo parts', exact: true }).click();
const product = page.locator("[data-mpn='BKR6E-11']");
await product.waitFor({ state: 'visible' });
if (await product.count() !== 1) throw new Error('Ambiguous part match');
const available = Number(await product.getAttribute('data-stock'));
const quantity = 16;
if (!Number.isSafeInteger(available) || available < quantity) {
throw new Error('The allocated quantity is not confirmed available');
}
await product.getByLabel('Quantity', { exact: true }).fill(String(quantity));
await page.locator("xpath=//*[@data-mpn='BKR6E-11']//button[@data-action='add-to-cart']")
.click();
const cart = page.locator('#demo-cart');
await cart.waitFor({ state: 'visible' });
const actualPart = await cart.locator('[data-cart-mpn]').textContent();
const actualQty = Number(await cart.locator('[data-cart-quantity]').textContent());
if (actualPart !== 'BKR6E-11' || actualQty !== quantity) {
throw new Error('Prepared cart does not match the allocation');
}
return { exampleOnly: true, manufacturerPart: actualPart, quantity: actualQty,
unit: 'each', merchandiseCents: actualQty * 320, submitted: false };
}
Documentation: Playwright: locators
Splitting the order across suppliers
In this example, NAPA has 16 of the 24 requested NGK plugs. Advance has the remaining eight of the same manufacturer part, plus the filters and wipers. The agent can propose that split. Substituting a different heat range, filter thread, or blade attachment would require an approved alternative.
The stock response needs the lookup time, account or store, quantity if known, and whether fulfillment means pickup, shipment, or backorder. Displayed availability may still need reservation. A site that only says "in stock" should produce an unknown quantity in the response.
Together, the two carts cover everything requested. Their combined total exceeds the $300 automatic-purchasing limit. Each cart is below it individually. The service therefore needs to evaluate the complete request before authorizing either purchase.
Two prepared carts / illustrative only
Mock carts with invented prices, stock, fees, tax, and references.
NAPA Auto Parts
DEMO-N-041 / Not submitted
- NGK V-Power spark plugBKR6E-1116 each x $3.20$51.20
- Merchandise
- $51.20
- Example shipping
- $12.00
- Example tax
- $3.58
- Cart total
- $66.78
Advance Auto Parts
DEMO-A-042 / Not submitted
- NGK V-Power spark plugBKR6E-118 each x $3.40$27.20
- FRAM Extra Guard oil filterPH361412 each x $5.50$66.00
- Bosch XpressFIT PRO wiper blade24FP8 each x $18.00$144.00
- Merchandise
- $237.20
- Example shipping
- $15.00
- Example tax
- $14.91
- Cart total
- $267.11
Hold for review
$333.89 combined total against a $300.00 example automatic-purchasing limit.
Together, the two carts cover everything requested: 24 plugs, 12 filters, and 8 blades. Together they exceed the limit, so the order needs review.
No orders submittedIllustrative exception: the price changes after approval. A reviewer approves $333.89 with no increase permitted. Before either cart is submitted, the service reads them again: Advance shipping has risen from $15.00 to $23.00, bringing the total to $341.89. It holds submission and returns the $8.00 increase for fresh approval in ODC. The agent can explain the change but cannot extend the approval.
Why I recommended an external Playwright service
OutSystems Developer Cloud was the proposed application environment. I recommended running supplier automation in an external service rather than embedding it in ODC Custom Code. Browser dependencies, sessions, queues, retries, supplier scripts, and debugging evidence needed their own execution lifecycle.
My concern with putting this inside C# extensions was the maintenance and diagnostic cost: too much processing could disappear behind an application call. This was a judgment about where to operate the workload, not a limitation of C#. A model-provided browser tool alone also left versioned workflows, secret isolation, purchasing authority, and order recovery to be designed.
An external service has its own running costs. I would compare cost per purchasing task, including model usage, browser runtime, storage, support, adapter maintenance, and platform charges under the actual contract. The prototype did not provide a measured production cost comparison.
Tool calls, credentials, and results
The proposed path was agent to MCP tool to a local automation server running Playwright. The request would identify the supplier and operation. The server would retrieve credentials from a secret store; Azure Key Vault is one option.
Even on a local server, authenticate the caller and authorize access to the company, supplier account, and order before retrieving secrets. Restrict browser navigation and network access to approved destinations. Expose specific purchasing operations instead of a general run-script tool.
The agent needs the matched part, quantities, prices, cart reference, adapter version, and exceptions. Passwords, tokens, and authenticated browser state stay with the service. Screenshots, DOM captures, traces, and logs need protection or redaction before leaving it. Playwright warns that saved browser state can allow impersonation.
Inspect the structured preparation result (illustrative JSON)
Amounts are integer cents. This result flags the order for review; approval is handled separately.
{
"exampleOnly": true,
"requestId": "DEMO-REQ-1042",
"adapterVersion": "demo-v1",
"status": "requires_review",
"currency": "USD",
"merchandiseCents": 28840,
"landedTotalCents": 33389,
"automaticLimitCents": 30000,
"approvalRequired": true,
"submitted": false,
"issues": [
"Combined landed total exceeds the example limit",
"Reviewer must reconcile both carts with the approved parts list"
],
"carts": [
{
"supplier": "NAPA Auto Parts",
"cartRef": "DEMO-N-041",
"lines": [
{
"manufacturerPart": "BKR6E-11",
"quantity": 16,
"unit": "each",
"unitPriceCents": 320
}
]
},
{
"supplier": "Advance Auto Parts",
"cartRef": "DEMO-A-042",
"lines": [
{
"manufacturerPart": "BKR6E-11",
"quantity": 8,
"unit": "each",
"unitPriceCents": 340
},
{
"manufacturerPart": "PH3614",
"quantity": 12,
"unit": "each",
"unitPriceCents": 550
},
{
"manufacturerPart": "24FP",
"quantity": 8,
"unit": "each",
"unitPriceCents": 1800
}
]
}
]
}Documentation: Azure Key Vault: secrets; Playwright: authentication state
Approval and order recovery
A business can authorize automatic purchases for approved parts, suppliers, destinations, quantities, and spending limits. Human-in-the-loop review handles exceptions before submission. Human-on-the-loop operation lets authorized work proceed under monitoring, with escalation and a way to pause future actions. The execution service enforces either policy.
The request total includes tax and shipping; unresolved charges hold up the order. If two orders run at the same time, the service needs to reserve each order's budget before proceeding. Otherwise, both could spend the same remaining allowance.
When someone approves an order, that approval belongs to the carts they reviewed: the parts, quantities, delivery destination, and permitted final amount. It also needs an expiry so an old decision cannot authorize a later purchase. Before submitting, the service reads the carts again. A material change, such as the shipping increase above, sends the order back for a new decision.
Pausing execution cannot cancel an order already accepted by a supplier. If submission times out, reconcile the order reference and supplier history before retrying. Track each supplier order separately: if NAPA accepts its order and Advance fails, replaying both carts risks buying the NAPA parts twice.
Documentation: OWASP: Excessive Agency
Work remaining after the prototype
The engagement stopped at a working prototype. In testing, we confirmed access to the external API and MCP tool registry; the full supplier workflow had not been validated end to end. Full purchasing-policy enforcement, monitoring, and recovery remained proposed production work; there are no production results or measured savings to report.
Before expanding, I would test wrong-part matches, pack-size errors, changed HTML, ambiguous selectors, expired sessions, and interrupted workers. The purchase path also needs tests for revoked approval, changed totals, concurrent budget use, duplicate submissions, and an order accepted just before a timeout.
Supplier terms and agreed account access determine which interfaces can be used. MFA, CAPTCHA, and denied actions need an approved authentication or manual process. The service must respect those controls even when they interrupt an otherwise working automation.
I would keep purchasing requests and review in ODC, with supplier automation in a service the team can test and maintain independently. That still leaves an adapter to maintain for each supplier.
Sources and further reading
- NAPA: NGK 2756 / BKR6E-11
Public part identification, thread size, and reach. No price or stock claim is taken from this listing.
- FRAM: PH3614 product specifications
Manufacturer-listed filter height, outside diameter, and thread dimensions.
- Bosch: XpressFIT PRO part lengths
Manufacturer reference for the 24FP, a 24-inch blade. Length alone is not fitment evidence.
- Playwright: locators
Role and label locators, XPath support, and selector limitations.
- Azure Key Vault: secrets
A possible server-side secret store, not a claim about the prototype's selected vendor.
- Playwright: authentication state
Why saved browser state must be handled as sensitive material.
- OWASP: Excessive Agency
Tool permissions, authority, and approval boundaries for agent actions.
- Advance Auto Parts
Named only as an illustrative supplier. Mock carts do not reproduce its live interface, stock, or prices.
- When not to use OutSystems for AI
The companion article on application boundaries and business-task-level cost.
Share this article
Continue the conversation
A question, a different experience, or an idea for the next article? Send Jason a private response.
