Match Buyer Orders to Catalog SKUs
Extract a buyer's natural-language order, then resolve each line item against your product catalog with a bem Collection — ERP/WMS-ready in one workflow call
If you're a produce supplier (or any wholesaler) ingesting buyer orders, the bottleneck isn't the API to your ERP — it's everything before the API. Buyers email POs in their own words: "10 cases organic gala apples, 88 ct" instead of APL-GALA-ORG-CASE × 10. Today, somebody types those lines into your order-entry screen by hand, looking up SKUs in a catalog tab, and your warehouse waits. This cookbook builds the small bem workflow that replaces that step.
You'll wire two function types into a single workflow:
- An Extract function that pulls a clean order shape — buyer, delivery date, line items — out of whatever the buyer sent (PDF, email, scan).
- An Enrich function that semantically matches each line item's free-text description to the right SKU in your Collection (your product catalog, indexed by bem).
The result is a single API call that turns "need 10 cases organic gala apples, 88 ct, Tuesday delivery" into:
{
"description": "organic gala apples, 88 ct",
"quantity": 10,
"unit": "case",
"matchedSKU": "APL-GALA-ORG-CASE",
"matchedName": "Organic Gala Apples",
"matchedPackSize": "88-count tray",
"unitCost": 42.00
}…ready to drop straight into the ERP or WMS. No string-matching, no fuzzy logic, no maintenance of a separate normalization table — the catalog is the index.
Pick a language from the tabs in each step. The flow is identical across cURL, the SDKs, and the CLI. If you don't have an SDK installed yet, see Step 2 of the Quickstart.
Prerequisites
-
A bem account and an API key from Settings → API Keys
-
BEM_API_KEYexported in your shell:export BEM_API_KEY='your-api-key-here' -
A sample order PDF on disk at
order-request.pdf. Any buyer's PO/email/order form will do — bem renders the file before extracting.
Step 1: Create the order Extract function
The schema below is intentionally produce-shaped. The lineItems[].description field stays as free text — that's deliberate, because the Enrich step in Step 4 is going to do the SKU lookup against that exact text. Don't over-engineer the schema by trying to pre-normalize the description; let the catalog do that work.
curl -X POST https://api.bem.ai/v3/functions \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"functionName": "order-extractor",
"type": "extract",
"displayName": "Buyer Order Extractor",
"tags": ["produce", "orders"],
"outputSchemaName": "BuyerOrder",
"outputSchema": {
"type": "object",
"required": ["orderNumber", "buyer", "lineItems"],
"properties": {
"orderNumber": { "type": "string", "description": "Buyer'"'"'s PO number, or a supplier-assigned ID if missing" },
"orderDate": { "type": "string", "description": "Date the order was placed (YYYY-MM-DD)" },
"requestedDeliveryDate": { "type": "string", "description": "Date the buyer wants delivery (YYYY-MM-DD)" },
"buyer": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Buyer'"'"'s company name" },
"accountNumber": { "type": "string", "description": "Buyer'"'"'s account on the supplier'"'"'s books, if present" },
"contact": { "type": "string", "description": "Buyer'"'"'s contact email or phone, if present" }
}
},
"deliveryAddress": { "type": "string", "description": "Full delivery address as a single string" },
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string", "description": "Line item as the buyer wrote it — e.g. '"'"'organic gala apples, 88 count tray'"'"'. Keep verbatim; downstream enrichment matches against this text." },
"quantity": { "type": "number", "description": "Number of units requested" },
"unit": { "type": "string", "description": "Unit of measure as written: case, lb, dozen, flat, clamshell, etc." },
"notes": { "type": "string", "description": "Buyer-supplied notes on this line — substitutions allowed, brand requests, etc." }
}
}
},
"notes": { "type": "string", "description": "Order-level notes — '"'"'leave at dock 4, call on arrival'"'"', etc." }
}
}
}'import Bem from "bem-ai-sdk";
const client = new Bem();
const { function: fn } = await client.functions.create({
functionName: "order-extractor",
type: "extract",
displayName: "Buyer Order Extractor",
tags: ["produce", "orders"],
outputSchemaName: "BuyerOrder",
outputSchema: {
type: "object",
required: ["orderNumber", "buyer", "lineItems"],
properties: {
orderNumber: { type: "string", description: "Buyer's PO number, or a supplier-assigned ID if missing" },
orderDate: { type: "string", description: "Date the order was placed (YYYY-MM-DD)" },
requestedDeliveryDate: { type: "string", description: "Date the buyer wants delivery (YYYY-MM-DD)" },
buyer: {
type: "object",
properties: {
name: { type: "string", description: "Buyer's company name" },
accountNumber: { type: "string", description: "Buyer's account on the supplier's books, if present" },
contact: { type: "string", description: "Buyer's contact email or phone, if present" },
},
},
deliveryAddress: { type: "string", description: "Full delivery address as a single string" },
lineItems: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string", description: "Line item as the buyer wrote it. Keep verbatim; downstream enrichment matches against this text." },
quantity: { type: "number", description: "Number of units requested" },
unit: { type: "string", description: "Unit of measure as written: case, lb, dozen, flat, clamshell, etc." },
notes: { type: "string", description: "Buyer-supplied notes on this line — substitutions allowed, brand requests, etc." },
},
},
},
notes: { type: "string", description: "Order-level notes — 'leave at dock 4, call on arrival', etc." },
},
},
});
console.log(fn);from bem import Bem
client = Bem()
response = client.functions.create(
function_name="order-extractor",
type="extract",
display_name="Buyer Order Extractor",
tags=["produce", "orders"],
output_schema_name="BuyerOrder",
output_schema={
"type": "object",
"required": ["orderNumber", "buyer", "lineItems"],
"properties": {
"orderNumber": {"type": "string", "description": "Buyer's PO number, or a supplier-assigned ID if missing"},
"orderDate": {"type": "string", "description": "Date the order was placed (YYYY-MM-DD)"},
"requestedDeliveryDate": {"type": "string", "description": "Date the buyer wants delivery (YYYY-MM-DD)"},
"buyer": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Buyer's company name"},
"accountNumber": {"type": "string", "description": "Buyer's account on the supplier's books, if present"},
"contact": {"type": "string", "description": "Buyer's contact email or phone, if present"},
},
},
"deliveryAddress": {"type": "string", "description": "Full delivery address as a single string"},
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string", "description": "Line item as the buyer wrote it. Keep verbatim; downstream enrichment matches against this text."},
"quantity": {"type": "number", "description": "Number of units requested"},
"unit": {"type": "string", "description": "Unit of measure as written: case, lb, dozen, flat, clamshell, etc."},
"notes": {"type": "string", "description": "Buyer-supplied notes on this line — substitutions allowed, brand requests, etc."},
},
},
},
"notes": {"type": "string", "description": "Order-level notes — 'leave at dock 4, call on arrival', etc."},
},
},
)
print(response.function)package main
import (
"context"
"fmt"
bem "github.com/bem-team/bem-go-sdk"
)
func main() {
client := bem.NewClient()
schema := map[string]any{
"type": "object",
"required": []string{"orderNumber", "buyer", "lineItems"},
"properties": map[string]any{
"orderNumber": map[string]any{"type": "string", "description": "Buyer's PO number, or a supplier-assigned ID if missing"},
"orderDate": map[string]any{"type": "string", "description": "Date the order was placed (YYYY-MM-DD)"},
"requestedDeliveryDate": map[string]any{"type": "string", "description": "Date the buyer wants delivery (YYYY-MM-DD)"},
"buyer": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"accountNumber": map[string]any{"type": "string"},
"contact": map[string]any{"type": "string"},
},
},
"deliveryAddress": map[string]any{"type": "string"},
"lineItems": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"description": map[string]any{"type": "string", "description": "Keep verbatim; downstream enrichment matches against this text."},
"quantity": map[string]any{"type": "number"},
"unit": map[string]any{"type": "string"},
"notes": map[string]any{"type": "string"},
},
},
},
"notes": map[string]any{"type": "string"},
},
}
resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
CreateFunction: bem.CreateFunctionUnionParam{
OfExtract: &bem.CreateFunctionExtractParam{
FunctionName: "order-extractor",
DisplayName: bem.String("Buyer Order Extractor"),
Tags: []string{"produce", "orders"},
OutputSchemaName: bem.String("BuyerOrder"),
OutputSchema: schema,
},
},
})
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", resp.Function)
}using System.Text.Json;
using Bem;
using Bem.Models.Functions;
BemClient client = new();
var schemaJson = """
{
"type": "object",
"required": ["orderNumber", "buyer", "lineItems"],
"properties": {
"orderNumber": { "type": "string", "description": "Buyer's PO number, or a supplier-assigned ID if missing" },
"orderDate": { "type": "string", "description": "Date the order was placed (YYYY-MM-DD)" },
"requestedDeliveryDate": { "type": "string", "description": "Date the buyer wants delivery (YYYY-MM-DD)" },
"buyer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"accountNumber": { "type": "string" },
"contact": { "type": "string" }
}
},
"deliveryAddress": { "type": "string" },
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string", "description": "Keep verbatim; downstream enrichment matches against this text." },
"quantity": { "type": "number" },
"unit": { "type": "string" },
"notes": { "type": "string" }
}
}
},
"notes": { "type": "string" }
}
}
""";
var response = await client.Functions.Create(new FunctionCreateParams
{
CreateFunction = new Extract
{
FunctionName = "order-extractor",
DisplayName = "Buyer Order Extractor",
Tags = new List<string> { "produce", "orders" },
OutputSchemaName = "BuyerOrder",
OutputSchema = JsonSerializer.Deserialize<JsonElement>(schemaJson),
},
});
Console.WriteLine(response.Function);bem functions create \
--function-name order-extractor \
--type extract \
--display-name "Buyer Order Extractor" \
--tag produce --tag orders \
--output-schema-name BuyerOrder \
--output-schema '{
"type": "object",
"required": ["orderNumber", "buyer", "lineItems"],
"properties": {
"orderNumber": { "type": "string" },
"orderDate": { "type": "string" },
"requestedDeliveryDate": { "type": "string" },
"buyer": { "type": "object", "properties": { "name": { "type": "string" }, "accountNumber": { "type": "string" }, "contact": { "type": "string" } } },
"deliveryAddress": { "type": "string" },
"lineItems": { "type": "array", "items": { "type": "object", "properties": { "description": { "type": "string" }, "quantity": { "type": "number" }, "unit": { "type": "string" }, "notes": { "type": "string" } } } },
"notes": { "type": "string" }
}
}'Step 2: Create a product catalog Collection
A Collection is bem's hosted, semantic-search index. You give it items (each with a data payload — string or object), and bem handles embedding, storage, and retrieval. You don't deploy a vector store, you don't tune a chunker, and you don't think about embeddings; the platform owns that surface so the rest of your team can think about catalog data, not infrastructure.
curl -X POST https://api.bem.ai/v3/collections \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{"collectionName": "produce_catalog"}'const collection = await client.collections.create({
collectionName: "produce_catalog",
});
console.log(collection);collection = client.collections.create(
collection_name="produce_catalog",
)
print(collection)collection, err := client.Collections.New(context.TODO(), bem.CollectionNewParams{
CollectionName: "produce_catalog",
})using Bem.Models.Collections;
var collection = await client.Collections.Create(new CollectionCreateParams
{
CollectionName = "produce_catalog",
});bem collections create --collection-name produce_catalogStep 3: Add SKUs to the collection
This is your product catalog. In production you'll sync it from your ERP or PIM nightly; for the cookbook, six representative SKUs are enough to demonstrate the matching behavior. Notice that each item's data is a structured object, not just a string — when bem matches a description, it returns the whole object, so the SKU, pack size, category, and unit cost all flow through to your downstream system on the same hop.
curl -X POST https://api.bem.ai/v3/collections/items \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"collectionName": "produce_catalog",
"items": [
{ "data": { "sku": "APL-GALA-ORG-CASE", "name": "Organic Gala Apples", "packSize": "88-count tray", "category": "Apples", "unitCost": 42.00 } },
{ "data": { "sku": "AVO-HASS-CASE-48", "name": "Hass Avocados", "packSize": "48-count case", "category": "Avocados", "unitCost": 38.50 } },
{ "data": { "sku": "SPN-BABY-CLAM-1LB", "name": "Baby Spinach", "packSize": "1 lb clamshell", "category": "Greens", "unitCost": 4.25 } },
{ "data": { "sku": "LEM-EUR-CASE-95", "name": "European Lemons", "packSize": "95-count case", "category": "Citrus", "unitCost": 28.00 } },
{ "data": { "sku": "LET-ROM-CASE-24", "name": "Romaine Hearts", "packSize": "24-count case", "category": "Greens", "unitCost": 22.50 } },
{ "data": { "sku": "STR-CON-FLAT-8", "name": "Conventional Strawberries", "packSize": "8x1 lb flat", "category": "Berries", "unitCost": 18.00 } }
]
}'const response = await client.collections.items.add({
collectionName: "produce_catalog",
items: [
{ data: { sku: "APL-GALA-ORG-CASE", name: "Organic Gala Apples", packSize: "88-count tray", category: "Apples", unitCost: 42.00 } },
{ data: { sku: "AVO-HASS-CASE-48", name: "Hass Avocados", packSize: "48-count case", category: "Avocados", unitCost: 38.50 } },
{ data: { sku: "SPN-BABY-CLAM-1LB", name: "Baby Spinach", packSize: "1 lb clamshell", category: "Greens", unitCost: 4.25 } },
{ data: { sku: "LEM-EUR-CASE-95", name: "European Lemons", packSize: "95-count case", category: "Citrus", unitCost: 28.00 } },
{ data: { sku: "LET-ROM-CASE-24", name: "Romaine Hearts", packSize: "24-count case", category: "Greens", unitCost: 22.50 } },
{ data: { sku: "STR-CON-FLAT-8", name: "Conventional Strawberries", packSize: "8x1 lb flat", category: "Berries", unitCost: 18.00 } },
],
});response = client.collections.items.add(
collection_name="produce_catalog",
items=[
{"data": {"sku": "APL-GALA-ORG-CASE", "name": "Organic Gala Apples", "packSize": "88-count tray", "category": "Apples", "unitCost": 42.00}},
{"data": {"sku": "AVO-HASS-CASE-48", "name": "Hass Avocados", "packSize": "48-count case", "category": "Avocados", "unitCost": 38.50}},
{"data": {"sku": "SPN-BABY-CLAM-1LB", "name": "Baby Spinach", "packSize": "1 lb clamshell", "category": "Greens", "unitCost": 4.25}},
{"data": {"sku": "LEM-EUR-CASE-95", "name": "European Lemons", "packSize": "95-count case", "category": "Citrus", "unitCost": 28.00}},
{"data": {"sku": "LET-ROM-CASE-24", "name": "Romaine Hearts", "packSize": "24-count case", "category": "Greens", "unitCost": 22.50}},
{"data": {"sku": "STR-CON-FLAT-8", "name": "Conventional Strawberries", "packSize": "8x1 lb flat", "category": "Berries", "unitCost": 18.00}},
],
)response, err := client.Collections.Items.Add(context.TODO(), bem.CollectionItemAddParams{
CollectionName: "produce_catalog",
Items: []bem.CollectionItemAddParamsItem{
{Data: map[string]any{"sku": "APL-GALA-ORG-CASE", "name": "Organic Gala Apples", "packSize": "88-count tray", "category": "Apples", "unitCost": 42.00}},
{Data: map[string]any{"sku": "AVO-HASS-CASE-48", "name": "Hass Avocados", "packSize": "48-count case", "category": "Avocados", "unitCost": 38.50}},
{Data: map[string]any{"sku": "SPN-BABY-CLAM-1LB", "name": "Baby Spinach", "packSize": "1 lb clamshell", "category": "Greens", "unitCost": 4.25}},
{Data: map[string]any{"sku": "LEM-EUR-CASE-95", "name": "European Lemons", "packSize": "95-count case", "category": "Citrus", "unitCost": 28.00}},
{Data: map[string]any{"sku": "LET-ROM-CASE-24", "name": "Romaine Hearts", "packSize": "24-count case", "category": "Greens", "unitCost": 22.50}},
{Data: map[string]any{"sku": "STR-CON-FLAT-8", "name": "Conventional Strawberries", "packSize": "8x1 lb flat", "category": "Berries", "unitCost": 18.00}},
},
})using System.Text.Json;
using Bem.Models.Collections.Items;
var response = await client.Collections.Items.Add(new ItemAddParams
{
CollectionName = "produce_catalog",
Items = new List<ItemAddParamsItem>
{
new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
{"sku": "APL-GALA-ORG-CASE", "name": "Organic Gala Apples", "packSize": "88-count tray", "category": "Apples", "unitCost": 42.00}
"""))),
new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
{"sku": "AVO-HASS-CASE-48", "name": "Hass Avocados", "packSize": "48-count case", "category": "Avocados", "unitCost": 38.50}
"""))),
new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
{"sku": "SPN-BABY-CLAM-1LB", "name": "Baby Spinach", "packSize": "1 lb clamshell", "category": "Greens", "unitCost": 4.25}
"""))),
new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
{"sku": "LEM-EUR-CASE-95", "name": "European Lemons", "packSize": "95-count case", "category": "Citrus", "unitCost": 28.00}
"""))),
new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
{"sku": "LET-ROM-CASE-24", "name": "Romaine Hearts", "packSize": "24-count case", "category": "Greens", "unitCost": 22.50}
"""))),
new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
{"sku": "STR-CON-FLAT-8", "name": "Conventional Strawberries", "packSize": "8x1 lb flat", "category": "Berries", "unitCost": 18.00}
"""))),
},
});bem collections:items add \
--collection-name produce_catalog \
--item '{data: {sku: APL-GALA-ORG-CASE, name: "Organic Gala Apples", packSize: "88-count tray", category: Apples, unitCost: 42.00}}' \
--item '{data: {sku: AVO-HASS-CASE-48, name: "Hass Avocados", packSize: "48-count case", category: Avocados, unitCost: 38.50}}' \
--item '{data: {sku: SPN-BABY-CLAM-1LB, name: "Baby Spinach", packSize: "1 lb clamshell", category: Greens, unitCost: 4.25}}' \
--item '{data: {sku: LEM-EUR-CASE-95, name: "European Lemons", packSize: "95-count case", category: Citrus, unitCost: 28.00}}' \
--item '{data: {sku: LET-ROM-CASE-24, name: "Romaine Hearts", packSize: "24-count case", category: Greens, unitCost: 22.50}}' \
--item '{data: {sku: STR-CON-FLAT-8, name: "Conventional Strawberries", packSize: "8x1 lb flat", category: Berries, unitCost: 18.00}}'The response is an async pending ack — bem embeds the items in the background. For a six-item catalog that finishes in seconds; for a 50,000-SKU production catalog allow a few minutes on the first sync. Subsequent updates are incremental.
Step 4: Create the SKU-matching Enrich function
The Enrich function is configured by where to look (sourceField), what catalog to search (collectionName), and where to put the result (targetField). The expression lineItems[*].description says "for every line item, take its description and search the catalog with it" — so the function iterates the order automatically. topK: 1 returns the single best match per line item.
curl -X POST https://api.bem.ai/v3/functions \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"functionName": "sku-resolver",
"type": "enrich",
"displayName": "Order Line Item → SKU Resolver",
"tags": ["produce", "sku-lookup"],
"config": {
"steps": [
{
"sourceField": "lineItems[*].description",
"collectionName": "produce_catalog",
"targetField": "matchedProducts",
"topK": 1,
"searchMode": "semantic"
}
]
}
}'const { function: fn } = await client.functions.create({
functionName: "sku-resolver",
type: "enrich",
displayName: "Order Line Item → SKU Resolver",
tags: ["produce", "sku-lookup"],
config: {
steps: [
{
sourceField: "lineItems[*].description",
collectionName: "produce_catalog",
targetField: "matchedProducts",
topK: 1,
searchMode: "semantic",
},
],
},
});response = client.functions.create(
function_name="sku-resolver",
type="enrich",
display_name="Order Line Item → SKU Resolver",
tags=["produce", "sku-lookup"],
config={
"steps": [
{
"sourceField": "lineItems[*].description",
"collectionName": "produce_catalog",
"targetField": "matchedProducts",
"topK": 1,
"searchMode": "semantic",
}
],
},
)resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
CreateFunction: bem.CreateFunctionUnionParam{
OfEnrich: &bem.CreateFunctionEnrichParam{
FunctionName: "sku-resolver",
DisplayName: bem.String("Order Line Item → SKU Resolver"),
Tags: []string{"produce", "sku-lookup"},
Config: bem.EnrichConfigParam{
Steps: []bem.EnrichStepParam{
{
SourceField: "lineItems[*].description",
CollectionName: "produce_catalog",
TargetField: "matchedProducts",
TopK: bem.Int(1),
SearchMode: bem.EnrichStepSearchModeSemantic,
},
},
},
},
},
})var response = await client.Functions.Create(new FunctionCreateParams
{
CreateFunction = new Enrich
{
FunctionName = "sku-resolver",
DisplayName = "Order Line Item → SKU Resolver",
Tags = new List<string> { "produce", "sku-lookup" },
Config = new EnrichConfig
{
Steps = new List<EnrichStep>
{
new EnrichStep
{
SourceField = "lineItems[*].description",
CollectionName = "produce_catalog",
TargetField = "matchedProducts",
TopK = 1,
SearchMode = EnrichStepSearchMode.Semantic,
},
},
},
},
});bem functions create \
--function-name sku-resolver \
--type enrich \
--display-name "Order Line Item → SKU Resolver" \
--tag produce --tag sku-lookup \
--config '{steps: [{sourceField: "lineItems[*].description", collectionName: produce_catalog, targetField: matchedProducts, topK: 1, searchMode: semantic}]}'Step 5: Create the workflow
Wire order-extractor and sku-resolver into a two-node DAG. The extractor is the entry point; the enricher reads its output and is the terminal node, so its enrichedContent is what callers receive.
Workflow: order-intake
+-----------------------------------------------------------------+
| |
| +-------------------+ +-----------------------+ |
| | order-extractor | ---------> | sku-resolver | |
| | (main) | | (enrich → catalog) | |
| +-------------------+ +-----------------------+ |
| |
+-----------------------------------------------------------------+
^
|
Buyer's PO (PDF, email, scan)curl -X POST https://api.bem.ai/v3/workflows \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"name": "order-intake",
"displayName": "Buyer Order Intake",
"tags": ["produce", "orders"],
"mainNodeName": "order-extractor",
"nodes": [
{ "name": "order-extractor", "function": { "name": "order-extractor" } },
{ "name": "sku-resolver", "function": { "name": "sku-resolver" } }
],
"edges": [
{ "sourceNodeName": "order-extractor", "destinationNodeName": "sku-resolver" }
]
}'const { workflow } = await client.workflows.create({
name: "order-intake",
displayName: "Buyer Order Intake",
tags: ["produce", "orders"],
mainNodeName: "order-extractor",
nodes: [
{ name: "order-extractor", function: { name: "order-extractor" } },
{ name: "sku-resolver", function: { name: "sku-resolver" } },
],
edges: [
{ sourceNodeName: "order-extractor", destinationNodeName: "sku-resolver" },
],
});response = client.workflows.create(
name="order-intake",
display_name="Buyer Order Intake",
tags=["produce", "orders"],
main_node_name="order-extractor",
nodes=[
{"name": "order-extractor", "function": {"name": "order-extractor"}},
{"name": "sku-resolver", "function": {"name": "sku-resolver"}},
],
edges=[
{"source_node_name": "order-extractor", "destination_node_name": "sku-resolver"},
],
)resp, err := client.Workflows.New(context.TODO(), bem.WorkflowNewParams{
Name: "order-intake",
DisplayName: bem.String("Buyer Order Intake"),
Tags: []string{"produce", "orders"},
MainNodeName: "order-extractor",
Nodes: []bem.WorkflowNewParamsNode{
{Name: bem.String("order-extractor"), Function: bem.FunctionVersionIdentifierParam{Name: bem.String("order-extractor")}},
{Name: bem.String("sku-resolver"), Function: bem.FunctionVersionIdentifierParam{Name: bem.String("sku-resolver")}},
},
Edges: []bem.WorkflowNewParamsEdge{
{SourceNodeName: "order-extractor", DestinationNodeName: "sku-resolver"},
},
})using Bem.Models.Workflows;
var response = await client.Workflows.Create(new WorkflowCreateParams
{
Name = "order-intake",
DisplayName = "Buyer Order Intake",
Tags = new List<string> { "produce", "orders" },
MainNodeName = "order-extractor",
Nodes = new List<Node>
{
new Node { Name = "order-extractor", Function = new FunctionVersionIdentifier { Name = "order-extractor" } },
new Node { Name = "sku-resolver", Function = new FunctionVersionIdentifier { Name = "sku-resolver" } },
},
Edges = new List<Edge>
{
new Edge { SourceNodeName = "order-extractor", DestinationNodeName = "sku-resolver" },
},
});bem workflows create \
--name order-intake \
--display-name "Buyer Order Intake" \
--tag produce --tag orders \
--main-node-name order-extractor \
--node '{name: order-extractor, function: {name: order-extractor}}' \
--node '{name: sku-resolver, function: {name: sku-resolver}}' \
--edge '{sourceNodeName: order-extractor, destinationNodeName: sku-resolver}'Step 6: Call the workflow with a buyer's order
Send order-request.pdf through the workflow with wait=true to get the SKU-resolved order back synchronously.
Multipart form data (recommended for files):
curl -X POST "https://api.bem.ai/v3/workflows/order-intake/call" \
-H "x-api-key: $BEM_API_KEY" \
-F "wait=true" \
-F "callReferenceID=PO-78421" \
-F "file=@order-request.pdf"Or, JSON body with base64-encoded file:
curl -X POST "https://api.bem.ai/v3/workflows/order-intake/call?wait=true" \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"callReferenceID": "PO-78421",
"input": {
"singleFile": {
"inputType": "pdf",
"inputContent": "'"$(base64 -i order-request.pdf)"'"
}
}
}'import fs from "node:fs";
const inputContent = fs.readFileSync("order-request.pdf").toString("base64");
const { call } = await client.workflows.call("order-intake", {
wait: true,
callReferenceID: "PO-78421",
input: {
singleFile: {
inputType: "pdf",
inputContent,
},
},
});
console.log(call?.status, call?.outputs);import base64
with open("order-request.pdf", "rb") as f:
input_content = base64.b64encode(f.read()).decode()
response = client.workflows.call(
"order-intake",
wait=True,
call_reference_id="PO-78421",
input={
"single_file": {
"input_type": "pdf",
"input_content": input_content,
}
},
)
print(response.call.status, response.call.outputs)data, err := os.ReadFile("order-request.pdf")
if err != nil {
panic(err)
}
encoded := base64.StdEncoding.EncodeToString(data)
resp, err := client.Workflows.Call(context.TODO(), "order-intake", bem.WorkflowCallParams{
Wait: bem.Bool(true),
CallReferenceID: bem.String("PO-78421"),
Input: bem.WorkflowCallParamsInput{
SingleFile: &bem.WorkflowCallParamsInputSingleFile{
InputType: "pdf",
InputContent: encoded,
},
},
})
if err != nil {
panic(err)
}
fmt.Printf("status=%s outputs=%d\n", resp.Call.Status, len(resp.Call.Outputs))var bytes = File.ReadAllBytes("order-request.pdf");
var encoded = Convert.ToBase64String(bytes);
var response = await client.Workflows.Call("order-intake", new WorkflowCallParams
{
Wait = true,
CallReferenceID = "PO-78421",
Input = new Input
{
SingleFile = new SingleFile
{
InputType = "pdf",
InputContent = encoded,
},
},
});
Console.WriteLine(response.Call.Status);bem workflows call \
--workflow-name order-intake \
--wait \
--call-reference-id PO-78421 \
--input.single-file '{"inputContent": "@order-request.pdf", "inputType": "pdf"}'Response:
{
"call": {
"callID": "wc_5gh789ijk",
"callReferenceID": "PO-78421",
"status": "completed",
"workflowName": "order-intake",
"workflowVersionNum": 1,
"outputs": [
{
"eventID": "ev_…",
"eventType": "enrich",
"functionName": "sku-resolver",
"enrichedContent": {
"orderNumber": "78421",
"orderDate": "2026-04-28",
"requestedDeliveryDate": "2026-04-30",
"buyer": {
"name": "Bayside Co-op",
"accountNumber": "BAYS-241",
"contact": "ordering@baysidecoop.com"
},
"deliveryAddress": "1100 Marina Way, Oakland, CA 94607",
"lineItems": [
{ "description": "organic gala apples, 88 ct", "quantity": 10, "unit": "case" },
{ "description": "Hass avocados, 48s", "quantity": 6, "unit": "case" },
{ "description": "baby spinach", "quantity": 12, "unit": "lb" },
{ "description": "European lemons, 95 ct", "quantity": 4, "unit": "case" },
{ "description": "romaine hearts", "quantity": 8, "unit": "case" },
{ "description": "strawberries, 8x1 lb flat", "quantity": 6, "unit": "flat" }
],
"matchedProducts": [
{ "data": { "sku": "APL-GALA-ORG-CASE", "name": "Organic Gala Apples", "packSize": "88-count tray", "category": "Apples", "unitCost": 42.00 }, "cosineDistance": 0.0612 },
{ "data": { "sku": "AVO-HASS-CASE-48", "name": "Hass Avocados", "packSize": "48-count case", "category": "Avocados", "unitCost": 38.50 }, "cosineDistance": 0.0784 },
{ "data": { "sku": "SPN-BABY-CLAM-1LB", "name": "Baby Spinach", "packSize": "1 lb clamshell", "category": "Greens", "unitCost": 4.25 }, "cosineDistance": 0.0931 },
{ "data": { "sku": "LEM-EUR-CASE-95", "name": "European Lemons", "packSize": "95-count case", "category": "Citrus", "unitCost": 28.00 }, "cosineDistance": 0.0593 },
{ "data": { "sku": "LET-ROM-CASE-24", "name": "Romaine Hearts", "packSize": "24-count case", "category": "Greens", "unitCost": 22.50 }, "cosineDistance": 0.1207 },
{ "data": { "sku": "STR-CON-FLAT-8", "name": "Conventional Strawberries", "packSize": "8x1 lb flat", "category": "Berries", "unitCost": 18.00 }, "cosineDistance": 0.0455 }
]
}
}
],
"errors": [],
"url": "/v3/calls/wc_5gh789ijk",
"traceUrl": "/v3/calls/wc_5gh789ijk/trace"
}
}matchedProducts[i] lines up positionally with lineItems[i] — the first match is for the first line, the second for the second, and so on. Each entry carries the full data object you stored on the catalog item plus a cosineDistance that gives you a soft confidence score (smaller = closer match). For tighter quality control, raise an exception flag below a threshold (e.g. anything ≥ 0.20) and route those orders to a human reviewer.
Because the terminal node here is an Enrich function, the SKU-resolved order lives at call.outputs[0].enrichedContent (not transformedContent — that's the field name for Extract/Transform/Join terminals). For the per-event-type field map and accessor patterns in every SDK, see Reading workflow call outputs.
What just happened
It's worth slowing down on the moving parts here, because the same shape works for every variation of "natural-language order intake → SKU resolution" that suppliers in produce, building materials, foodservice, pharma distribution, and parts run into.
- The Extract function did not need to be told what an order is. Every field's
descriptiontells the LLM what to look for. The schema acts as both contract and prompt — you don't maintain a separate prompt file. - The Enrich function did not chunk, embed, or index anything itself. When you added items to
produce_catalog, bem embedded eachdatapayload server-side with a managed embedding model. The function just submits the line item description and gets the top match back. - The DAG ran in the right order automatically.
extract → enrichbecause the edge says so. Workflow versions are immutable, so adding a third step later (say, apayload_shapingnode that reformats the result for your ERP) creates a v2 without disrupting any caller still on v1. - The same
wait=truepath that returned this result is also what you'd subscribe to with a webhook — so the dev-loop call and the production hand-off use the same shape.
Wiring this into your ERP or WMS
The terminal enrichedContent is the payload you send downstream. Two common patterns:
- Webhook subscription. Subscribe a URL in your service (NetSuite middleware, SAP integration layer, a custom Lambda) to the workflow's terminal events. bem POSTs the same JSON shape you saw above the moment a call finishes, signed with HMAC-SHA256 so you can verify it. See Webhooks.
- Add a Send node to push results directly to a webhook URL, S3 bucket, or Google Drive folder — useful when the receiving system is fine with file-drop semantics (S3 → SFTP-out → WMS, for example).
For ERPs specifically, the typical line-of-business shape is:
| Field needed by the ERP | Where it comes from |
|---|---|
| Buyer account ID | enrichedContent.buyer.accountNumber (Extract) |
| Requested ship date | enrichedContent.requestedDeliveryDate (Extract) |
(SKU, quantity, unit cost) per line | Zip lineItems[i].quantity with matchedProducts[i].data.sku and matchedProducts[i].data.unitCost |
| Confidence flag | matchedProducts[i].cosineDistance (route to human review if above threshold) |
If your ERP needs a particular wire format (cXML, EDI 850, a custom JSON shape), drop a Payload Shaping node at the end of the workflow and reshape with JMESPath. No code, no separate ETL job.
Next steps
Enrich Functions
Full reference: search modes, multi-step enrichment, multiple collections per function
Extract Functions
Schema design, tabular chunking, visual vs text-first inputs
Webhooks
Subscribe an endpoint and verify signed deliveries
Schema building guide
Designing outputSchema for reliable extraction