Back to Library
Architecture

RFQ Engine Architecture: Why Availability Holds and Cancellation Snapshots Matter

Last updated: July 13, 2026

The problem most RFQ automation misses

When teams automate RFQ workflows, they typically focus on the front end: parse the buyer request, search the catalog, match products, apply pricing. That work is real, and tools exist for it — AI extracts requirements while deterministic rules handle pricing, approvals, and SLA tracking, as GEP describes in its procurement automation analysis. Ivalua's 2026 sourcing guide confirms the same pattern: AI handles extraction and matching, rules handle governance.

But two problems break this model in production, and most automation does not address them:

  1. Availability drift. A quote that takes three days to negotiate is a quote against inventory that may no longer exist at the offered price. If the agent promises 50 units at $12.40 and the supplier has 12 left when the buyer accepts, the quote was never deliverable. The agent did not lie — the world changed underneath it.

  2. Policy drift. A cancellation penalty quoted at 30 days out may differ from the penalty in force at booking. If the supplier tightens terms mid-negotiation and the agent does not capture the policy at quote time, the buyer disputes the charge later. The dispute is legitimate — the terms moved.

The architecture that solves both is an RFQ engine with two mechanisms: atomic availability holds that reserve constrained inventory for a time window, and cancellation policy snapshots that freeze the commercial terms at the moment of quote. These are not features bolted onto a quoting tool. They are structural commitments in the data model — fields, status transitions, and lifecycle operations that the engine enforces.

The entity model that makes it work

A production RFQ engine needs more than a request and a quote. The entity model must separate the buyer's intent from the supplier's response, and it must track constrained supply independently of the catalog. The following entities form the backbone:

Request — the buyer's intent: contact, title, line items, billing and shipping addresses, expiration, status. A request moves through a defined state machine: initialin_progressconfirmedcompleted, with modified as a re-entry point when items change after quotes exist.

Item — the catalog product the buyer wants. Items are generic; they have a UUID, type, pricing mode, and units of measure. The same item maps to multiple provider items.

ProviderItem — the supplier's specific offering of that item, with a base price per unit of measure. A provider item belongs to a provider corporation and can have multiple batches.

ProviderItemBatch — the constrained inventory unit. Each batch has a batch number, production date, expiration date, cost per unit, in-stock flag, and a slow_move_item flag for slow-moving inventory. Batches are the granularity at which availability is checked and held.

Quote — the supplier's priced response to a confirmed request. A quote has a provider, a sales rep, shipping method and amount, currency, FX rate and FX lock timestamp, negotiation round count (auto-calculated), and a status machine: initialin_progressconfirmedcompleted or disapproved.

QuoteItem — a line on the quote, linking a provider item and batch to a quantity, price per unit, subtotal, discount, and final subtotal. Critically, the quote item carries hold_token, hold_expires_at, guardrail_price_per_uom, and slow_move_item — the fields that make availability holds and profitability guardrails visible at the line level.

CancellationPolicy — the terms under which a booking can be cancelled, with tiers, deadlines, and penalty rules, scoped per provider item.

Installment — the payment schedule linked to a confirmed quote, with priority, scheduled date, amount, ratio, and a status machine: pendingpaid or cancelled.

This is not a flat table. It is a typed graph with nested resolvers — a request resolves its quotes, a quote resolves its items, a quote item resolves its provider item and batch. The engine uses DataLoader-based batch loading so a quote with 20 line items does not fire 20 sequential database lookups.

Availability holds: reserving capacity atomically

The availability hold is the mechanism that prevents availability drift. Without it, an agent quotes against a snapshot of inventory that may be stale by the time the buyer decides. With it, the agent reserves capacity for a time window and the quote is backed by real, decremented supply.

The hold lifecycle has four states and strict transition rules:

State Meaning Allowed transitions
held Capacity decremented, hold token issued with a TTL (15 minutes) confirmed, released, expired
confirmed Booking committed, no second capacity decrement terminal
released Hold cancelled, capacity restored (idempotent) terminal
expired TTL elapsed, capacity restored (idempotent) terminal

The critical design decision is in the confirmed transition: no second capacity decrement. The hold already reserved the capacity. Confirming the booking converts the hold to a confirmed state without touching the inventory count again. This prevents double-counting — the bug where a hold decrements capacity, then a confirmation decrements it again, and the system thinks it has less supply than it does.

The hold is atomic. The acquireAvailabilityHold mutation decrements available capacity and returns a hold_token with an expires_at timestamp. If capacity is insufficient, the operation fails without side effects — no partial hold, no dirty state. The releaseAvailabilityHold and expireAvailabilityHold mutations restore capacity and are idempotent: calling them twice on the same token returns the same result. This matters for fault tolerance — if a network timeout causes a retry, the system does not over-restore capacity.

How this maps to a quoting workflow

Consider a distributor quoting 200 units of an industrial pump from a supplier who has three batches with different expiration dates and costs. The agent:

  1. Calls check_availability with the provider item UUID, service window, and quantity — gets matched batches and available quantity.
  2. Calls acquire_availability_hold — capacity is decremented atomically, a hold_token with a 15-minute TTL is returned.
  3. The quote item now carries hold_token and hold_expires_at. The buyer sees a quote backed by real inventory, not a catalog guess.
  4. If the buyer accepts within 15 minutes, the agent calls confirm_availability_hold — the hold transitions to confirmed, capacity is locked, no second decrement.
  5. If the buyer declines or the TTL expires, the agent calls release_availability_hold or lets it expire — capacity is restored, the next quote can use it.

The hold_token is an explicit handle. The model passes it as an argument, not as hidden session state. This is the same explicit-handle pattern the MCP 2026-07-28 release candidate formalizes: state that an agent needs across tool calls becomes a visible, auditable handle rather than transport-layer metadata.

Cancellation snapshots: freezing the terms

Cancellation policy drift is the second failure mode. A supplier may change cancellation penalties between quote and booking — a 10% penalty at 30 days out becomes 50% at 7 days out. If the agent quoted the old penalty and the buyer accepted, the buyer disputes the new charge. The dispute is correct.

The architecture solves this by snapshotting the cancellation policy at quote time. The CancellationPolicy entity has tiers (a JSON structure with deadlines and penalty percentages), a label, a description, and a status. When the agent generates a quote, it retrieves the active cancellation policy for each provider item and attaches the policy terms to the quote. The policy UUID and its tiers are frozen — even if the supplier updates the policy later, the quote carries the terms that were in force when it was issued.

The search_cancellation_policies tool filters by provider item and status, so the agent can retrieve the active policy for each line item. The policy is then associated with the quote item, not just referenced by a foreign key — the snapshot is the policy, not a pointer to a mutable record.

This pattern generalizes beyond cancellation. FX rates use the same approach: the QuoteType carries fx_rate and fx_rate_locked_at — the exchange rate is locked at quote time, not looked up at booking. If the buyer's display currency differs from the supplier's quote currency, the locked rate is the rate the buyer sees, regardless of market movement.

Pricing tiers, segment lookup, and guardrails

Pricing is not a flat field. The engine resolves price tiers by customer segment, quantity, and provider item. The get_item_price_tiers tool takes a customer email and a list of quote items (each with item UUID, provider item UUID, and quantity), looks up the customer's segment via segment contacts, and batch-loads all matching price tiers in a single query. The tiers are filtered by quantity thresholds at the database level — only tiers where quantity_greater_then and quantity_less_then bracket the requested quantity are returned.

Discount prompts work the same way, with a hierarchical scope system: GLOBAL (applies to all quotes), SEGMENT (applies to a customer segment), ITEM (applies to a specific item), and PROVIDER_ITEM (applies to a specific provider's offering of an item). The get_discount_prompts tool loads from all four scopes, deduplicates, and returns the combined set with conditions and rules. This means a quote can apply a volume discount (segment scope), a seasonal promotion (item scope), and a supplier-specific clearance (provider item scope) simultaneously — each with its own priority.

The guardrail_price_per_uom field on the quote item is the profitability guardrail — the minimum acceptable price for the item, loaded from the provider item batch. If the agent applies a discount that drops the price below the guardrail, the operation does not fail (the agent may have a legitimate reason), but the flag is visible in the quote item response. The slow_move_item flag marks inventory that has been in stock too long — the agent can use this to apply deeper discounts, but the guardrail prevents unprofitable pricing.

The status machines that prevent invalid operations

An RFQ engine without status enforcement is a spreadsheet. The engine enforces four state machines, each with explicit transition rules and operation guards:

Request: initialin_progressconfirmedcompleted, with modified as a re-entry point. The RequestOperationGuard enforces that quotes can only be created from confirmed requests, and item modifications are only allowed in initial, in_progress, or modified states.

Quote: initialin_progressconfirmedcompleted or disapproved. The QuoteOperationGuard enforces that item modifications are only allowed in initial or in_progress, and installments can only be created from confirmed quotes.

Availability Hold: heldconfirmed / released / expired. All three target states are terminal.

Installment: pendingpaid or cancelled. Both target states are terminal.

When a confirmed request's items change and quotes already exist, the request transitions to modified and all existing quotes are automatically disapproved — the buyer must get new quotes against the updated items. This prevents stale quotes from being accepted after the request that generated them has changed.

When all non-cancelled installments on a quote are paid, the quote auto-transitions to completed. When at least one quote on a request is completed, the request auto-transitions to completed. The status machines are not advisory — the engine validates every transition and raises a VALIDATION_FAILED error with the current status and allowed transitions if the operation is invalid.

The module pattern: 38 tools, 11 domain mixins

The reference implementation of this architecture is a single MCP module — mcp_hospirfq_processor — with 38 registered tools composed from 11 domain mixins: Request, Item, Availability, Quote, Pricing, Installment, Bundle, Cancellation, File, Segment, and Catalog. The mixins share a GraphQLBackedProcessor base class that provides the GraphQL client, logger, and settings. Each mixin contributes its own tool methods and accesses only self.logger, self.setting, and self._execute_graphql_query.

The tools are registered through a configuration block (MCP_CONFIGURATION) that declares each tool's name, description, input schema (JSON Schema), and module link (class name, function name, return type). The MCP runtime loads this configuration at startup and dispatches tool calls to the corresponding mixin methods. The @handle_errors decorator wraps every tool call with structured error handling, producing typed error responses with codes like GRAPHQL_QUERY_FAILED, VALIDATION_FAILED, HOLD_NOT_FOUND, HOLD_ALREADY_EXPIRED, AVAILABILITY_INSUFFICIENT, and PRICING_MODE_UNSUPPORTED.

The 38 tools span the full RFQ lifecycle:

Domain Tools Key operations
Request management 8 Submit, update, get, search, add/remove items, assign/remove provider items
Item management 3 Search items, get item, get provider items with batch info
Quote management 4 Update quote, get quote, search quotes, update quote item
Pricing 3 Get price tiers, get discount prompts, calculate quote pricing
Installment 4 Create, update, create schedule, get installments
Availability 5 Check, acquire hold, release, confirm, expire
Bundle 3 Search bundles, get bundle, search components
Cancellation 2 Get policy, search policies
Catalog 1 Inquire catalog (knowledge graph search)
Segment 1 Get segment contacts
File 2 Upload, get files
Workflow 2 Confirm request + create quotes, confirm quote + create installments

The two workflow tools — confirm_request_and_create_quotes and confirm_quote_and_create_installments — are convenience operations that compose multiple atomic tools into a single call. The first confirms a request and creates quotes for selected providers in one operation. The second confirms a quote and creates the installment plan in one operation. These reduce the number of round trips an agent needs for the most common multi-step workflows.

The inquire_catalog tool is the bridge to the knowledge graph. It takes a natural language query, an optional namespace (hotel, flight, activity), and a limit, and returns ranked results from the knowledge graph engine. This is how the agent discovers products before quoting — not by SQL queries against a product table, but by semantic search across a catalog graph that knows substitutes, compatibility, and availability.

Why this architecture generalizes

The reference implementation was built for travel and hospitality — date-bounded inventory, occupancy rules, per-pax pricing, bundles, deposits, cancellation terms. That is the stress test. If the engine can quote and reserve a hotel room for specific dates, hold the availability with a TTL, snapshot the cancellation policy, lock the FX rate, apply segment-specific pricing tiers, and hand off to an installment plan, it can support any B2B RFQ workflow with suppliers, buyers, constrained inventory, and commercial terms.

The generalization is structural:

  • Distribution and wholesale: replace hotel rooms with warehouse stock, provider items with supplier SKUs, batches with lot numbers and expiration dates. The availability hold reserves stock; the cancellation snapshot preserves return terms.
  • Procurement: replace travel packages with parts and materials. The segment-based pricing tiers apply negotiated contract rates; the guardrail price prevents unprofitable sourcing.
  • Manufacturing: replace service windows with production slots. The availability hold reserves capacity on a production line; the cancellation policy preserves the rescheduling penalty.
  • Field services: replace hotel dates with appointment windows. The availability hold reserves a technician's time slot; the installment plan structures the payment schedule.

The entity model — Request, Item, ProviderItem, ProviderItemBatch, Quote, QuoteItem, CancellationPolicy, Installment — is industry-agnostic. The domain mixins are composable: a procurement deployment might drop the Bundle mixin and keep the rest. A manufacturing deployment might add a ProductionSlot mixin alongside Availability. The module pattern, the status machines, and the explicit-handle approach stay the same.


A distributor running NetSuite, BigCommerce, and three supplier catalogs gets an agent that receives an RFQ by email or portal, resolves products and substitutes against the catalog graph, prices per customer tier, holds stock with an expiry, snapshots the cancellation terms, locks the FX rate, and writes the accepted quote back to NetSuite — with every tool call logged and every status transition validated. That build is Phase 2-3 of the four-step method and is typically live in 5-8 weeks.

Request a scoped build. One-week discovery. You get a system inventory, workflow map, and fixed scope — whether or not you build with us.

Want this built for your systems?

Every document here comes from real production work. If you have a target system and a workflow in mind, we can scope a build in one week.

Request a scoped build

One-week discovery. You get a system inventory, workflow map, and fixed scope — whether or not you build with us.