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.

By Jason Marchalonis · Published

  • AI
  • OutSystems
  • Architecture
  • AI governance

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.

Supplier websites become versioned automation tools. An agent coordinates the order while an execution service protects credentials and enforces purchasing policy.
Architecture illustration of the proposal: supplier-specific automation behind a shared application interface. View full-size illustration

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

Real product references. Example quantities. All lines ordered as each.
PartIdentifiersPublished dimensionsRequest
NGK V-Power spark plugBKR6E-11
Catalog no. 2756
M14 x 1.25 thread; 19 mm reach
Product reference
24 each
FRAM Extra Guard oil filterPH3614
Catalog no. PH3614
3.430 in height; 2.984 in outside diameter; 3/4-16 thread
Product reference
12 each
Bosch XpressFIT PRO wiper blade24FP
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-demo.invalid Local mock / not a retailer site

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
supplier-demo.invalid Local mock / not a retailer site

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
supplier-demo.invalid Local mock / not a retailer site

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.

Mock supplier cart

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
Mock supplier cart

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 submitted

Illustrative 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

Share this article

LinkedIn (opens in a new tab)Email

Continue the conversation

A question, a different experience, or an idea for the next article? Send Jason a private response.

Share a question or experience
Where does supplier automation become difficult for your team?

Open this section to prepare the form.

Browse the full article archive