Triage and Extract Logistics Documents
Build a workflow that classifies inbound logistics docs (invoices, bills of lading, packing slips) and routes each to a type-specific Extract function
This cookbook builds an end-to-end logistics-document triage pipeline. By the end you will have:
- Three Extract functions — one each for invoices, bills of lading, and packing slips — with sensible schemas that capture the canonical fields of each document type
- A Classify function that decides which document type an inbound file is
- A workflow that wires them into a branching DAG: one entry point, three terminal outputs
- A working call that demonstrates the routing in action
The example uses logistics — invoices, bills of lading (BOLs), packing slips — but the same shape (classify → extract per type) works for any inbound document mix: claims vs benefit summaries vs prescriptions, contracts vs SOWs vs purchase orders, and so on.
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 few sample logistics PDFs on disk to call the workflow against:
invoice.pdf,bill-of-lading.pdf,packing-slip.pdf. Any PDF or image of each type works — bem renders the file before classifying.
Step 1: Create the invoice Extract function
Each document type gets its own Extract function so the schema reflects what's actually in that kind of document. Start with the invoice extractor.
curl -X POST https://api.bem.ai/v3/functions \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"functionName": "invoice-extractor",
"type": "extract",
"displayName": "Invoice Extractor",
"tags": ["logistics", "invoices"],
"outputSchemaName": "Invoice",
"outputSchema": {
"type": "object",
"required": ["invoiceNumber", "vendor", "totalAmount"],
"properties": {
"invoiceNumber": { "type": "string", "description": "Unique invoice identifier" },
"invoiceDate": { "type": "string", "description": "Invoice date (YYYY-MM-DD)" },
"dueDate": { "type": "string", "description": "Payment due date (YYYY-MM-DD)" },
"vendor": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "type": "string" }
}
},
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"unitPrice": { "type": "number" },
"amount": { "type": "number" }
}
}
},
"subtotal": { "type": "number" },
"totalAmount": { "type": "number" }
}
}
}'import Bem from "bem-ai-sdk";
const client = new Bem();
const { function: fn } = await client.functions.create({
functionName: "invoice-extractor",
type: "extract",
displayName: "Invoice Extractor",
tags: ["logistics", "invoices"],
outputSchemaName: "Invoice",
outputSchema: {
type: "object",
required: ["invoiceNumber", "vendor", "totalAmount"],
properties: {
invoiceNumber: { type: "string", description: "Unique invoice identifier" },
invoiceDate: { type: "string", description: "Invoice date (YYYY-MM-DD)" },
dueDate: { type: "string", description: "Payment due date (YYYY-MM-DD)" },
vendor: {
type: "object",
properties: {
name: { type: "string" },
address: { type: "string" },
},
},
lineItems: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string" },
quantity: { type: "number" },
unitPrice: { type: "number" },
amount: { type: "number" },
},
},
},
subtotal: { type: "number" },
totalAmount: { type: "number" },
},
},
});
console.log(fn);from bem import Bem
client = Bem()
response = client.functions.create(
function_name="invoice-extractor",
type="extract",
display_name="Invoice Extractor",
tags=["logistics", "invoices"],
output_schema_name="Invoice",
output_schema={
"type": "object",
"required": ["invoiceNumber", "vendor", "totalAmount"],
"properties": {
"invoiceNumber": {"type": "string", "description": "Unique invoice identifier"},
"invoiceDate": {"type": "string", "description": "Invoice date (YYYY-MM-DD)"},
"dueDate": {"type": "string", "description": "Payment due date (YYYY-MM-DD)"},
"vendor": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"type": "string"},
},
},
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unitPrice": {"type": "number"},
"amount": {"type": "number"},
},
},
},
"subtotal": {"type": "number"},
"totalAmount": {"type": "number"},
},
},
)
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{"invoiceNumber", "vendor", "totalAmount"},
"properties": map[string]any{
"invoiceNumber": map[string]any{"type": "string"},
"invoiceDate": map[string]any{"type": "string"},
"dueDate": map[string]any{"type": "string"},
"vendor": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"address": 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"},
"quantity": map[string]any{"type": "number"},
"unitPrice": map[string]any{"type": "number"},
"amount": map[string]any{"type": "number"},
},
},
},
"subtotal": map[string]any{"type": "number"},
"totalAmount": map[string]any{"type": "number"},
},
}
resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
CreateFunction: bem.CreateFunctionUnionParam{
OfExtract: &bem.CreateFunctionExtractParam{
FunctionName: "invoice-extractor",
DisplayName: bem.String("Invoice Extractor"),
Tags: []string{"logistics", "invoices"},
OutputSchemaName: bem.String("Invoice"),
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": ["invoiceNumber", "vendor", "totalAmount"],
"properties": {
"invoiceNumber": { "type": "string", "description": "Unique invoice identifier" },
"invoiceDate": { "type": "string", "description": "Invoice date (YYYY-MM-DD)" },
"dueDate": { "type": "string", "description": "Payment due date (YYYY-MM-DD)" },
"vendor": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "type": "string" }
}
},
"lineItems": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"unitPrice": { "type": "number" },
"amount": { "type": "number" }
}
}
},
"subtotal": { "type": "number" },
"totalAmount": { "type": "number" }
}
}
""";
var response = await client.Functions.Create(new FunctionCreateParams
{
CreateFunction = new Extract
{
FunctionName = "invoice-extractor",
DisplayName = "Invoice Extractor",
Tags = ["logistics", "invoices"],
OutputSchemaName = "Invoice",
OutputSchema = JsonSerializer.Deserialize<JsonElement>(schemaJson),
},
});
Console.WriteLine(response.Function);bem functions create \
--function-name invoice-extractor \
--type extract \
--display-name "Invoice Extractor" \
--tags '["logistics", "invoices"]' \
--output-schema-name Invoice \
--output-schema '{
"type": "object",
"required": ["invoiceNumber", "vendor", "totalAmount"],
"properties": {
"invoiceNumber": { "type": "string" },
"invoiceDate": { "type": "string" },
"dueDate": { "type": "string" },
"vendor": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"lineItems": { "type": "array", "items": { "type": "object", "properties": { "description": { "type": "string" }, "quantity": { "type": "number" }, "unitPrice": { "type": "number" }, "amount": { "type": "number" } } } },
"subtotal": { "type": "number" },
"totalAmount": { "type": "number" }
}
}'Step 2: Create the bill of lading Extract function
Bills of lading carry shipper, consignee, carrier, and per-line freight detail. Drop the invoice-shaped fields and add the freight ones.
curl -X POST https://api.bem.ai/v3/functions \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"functionName": "bol-extractor",
"type": "extract",
"displayName": "Bill of Lading Extractor",
"tags": ["logistics", "freight"],
"outputSchemaName": "BillOfLading",
"outputSchema": {
"type": "object",
"required": ["bolNumber", "shipper", "consignee"],
"properties": {
"bolNumber": { "type": "string", "description": "Bill of lading number (sometimes called the pro number)" },
"shipDate": { "type": "string", "description": "Ship date (YYYY-MM-DD)" },
"carrier": { "type": "string", "description": "Carrier name or SCAC code" },
"shipper": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "type": "string" }
}
},
"consignee": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "type": "string" }
}
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"weightLbs": { "type": "number" },
"freightClass": { "type": "string" }
}
}
},
"totalWeightLbs": { "type": "number" },
"freightCharges": { "type": "number" }
}
}
}'const { function: fn } = await client.functions.create({
functionName: "bol-extractor",
type: "extract",
displayName: "Bill of Lading Extractor",
tags: ["logistics", "freight"],
outputSchemaName: "BillOfLading",
outputSchema: {
type: "object",
required: ["bolNumber", "shipper", "consignee"],
properties: {
bolNumber: { type: "string", description: "Bill of lading number (sometimes called the pro number)" },
shipDate: { type: "string", description: "Ship date (YYYY-MM-DD)" },
carrier: { type: "string", description: "Carrier name or SCAC code" },
shipper: {
type: "object",
properties: {
name: { type: "string" },
address: { type: "string" },
},
},
consignee: {
type: "object",
properties: {
name: { type: "string" },
address: { type: "string" },
},
},
items: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string" },
quantity: { type: "number" },
weightLbs: { type: "number" },
freightClass: { type: "string" },
},
},
},
totalWeightLbs: { type: "number" },
freightCharges: { type: "number" },
},
},
});response = client.functions.create(
function_name="bol-extractor",
type="extract",
display_name="Bill of Lading Extractor",
tags=["logistics", "freight"],
output_schema_name="BillOfLading",
output_schema={
"type": "object",
"required": ["bolNumber", "shipper", "consignee"],
"properties": {
"bolNumber": {"type": "string", "description": "Bill of lading number (sometimes called the pro number)"},
"shipDate": {"type": "string", "description": "Ship date (YYYY-MM-DD)"},
"carrier": {"type": "string", "description": "Carrier name or SCAC code"},
"shipper": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"type": "string"},
},
},
"consignee": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"type": "string"},
},
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"weightLbs": {"type": "number"},
"freightClass": {"type": "string"},
},
},
},
"totalWeightLbs": {"type": "number"},
"freightCharges": {"type": "number"},
},
},
)schema := map[string]any{
"type": "object",
"required": []string{"bolNumber", "shipper", "consignee"},
"properties": map[string]any{
"bolNumber": map[string]any{"type": "string"},
"shipDate": map[string]any{"type": "string"},
"carrier": map[string]any{"type": "string"},
"shipper": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"address": map[string]any{"type": "string"},
},
},
"consignee": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"address": map[string]any{"type": "string"},
},
},
"items": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"description": map[string]any{"type": "string"},
"quantity": map[string]any{"type": "number"},
"weightLbs": map[string]any{"type": "number"},
"freightClass": map[string]any{"type": "string"},
},
},
},
"totalWeightLbs": map[string]any{"type": "number"},
"freightCharges": map[string]any{"type": "number"},
},
}
resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
CreateFunction: bem.CreateFunctionUnionParam{
OfExtract: &bem.CreateFunctionExtractParam{
FunctionName: "bol-extractor",
DisplayName: bem.String("Bill of Lading Extractor"),
Tags: []string{"logistics", "freight"},
OutputSchemaName: bem.String("BillOfLading"),
OutputSchema: schema,
},
},
})var schemaJson = """
{
"type": "object",
"required": ["bolNumber", "shipper", "consignee"],
"properties": {
"bolNumber": { "type": "string", "description": "Bill of lading number (sometimes called the pro number)" },
"shipDate": { "type": "string", "description": "Ship date (YYYY-MM-DD)" },
"carrier": { "type": "string", "description": "Carrier name or SCAC code" },
"shipper": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"consignee": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"weightLbs": { "type": "number" },
"freightClass": { "type": "string" }
}
}
},
"totalWeightLbs": { "type": "number" },
"freightCharges": { "type": "number" }
}
}
""";
var response = await client.Functions.Create(new FunctionCreateParams
{
CreateFunction = new Extract
{
FunctionName = "bol-extractor",
DisplayName = "Bill of Lading Extractor",
Tags = ["logistics", "freight"],
OutputSchemaName = "BillOfLading",
OutputSchema = JsonSerializer.Deserialize<JsonElement>(schemaJson),
},
});bem functions create \
--function-name bol-extractor \
--type extract \
--display-name "Bill of Lading Extractor" \
--tags '["logistics", "freight"]' \
--output-schema-name BillOfLading \
--output-schema '{
"type": "object",
"required": ["bolNumber", "shipper", "consignee"],
"properties": {
"bolNumber": { "type": "string" },
"shipDate": { "type": "string" },
"carrier": { "type": "string" },
"shipper": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"consignee": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"items": { "type": "array", "items": { "type": "object", "properties": { "description": { "type": "string" }, "quantity": { "type": "number" }, "weightLbs": { "type": "number" }, "freightClass": { "type": "string" } } } },
"totalWeightLbs": { "type": "number" },
"freightCharges": { "type": "number" }
}
}'Step 3: Create the packing slip Extract function
Packing slips track what physically shipped — recipient, items, quantities ordered vs shipped — without the financial detail of an invoice or the freight detail of a BOL.
curl -X POST https://api.bem.ai/v3/functions \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"functionName": "packing-slip-extractor",
"type": "extract",
"displayName": "Packing Slip Extractor",
"tags": ["logistics", "fulfillment"],
"outputSchemaName": "PackingSlip",
"outputSchema": {
"type": "object",
"required": ["packingSlipNumber", "recipient"],
"properties": {
"packingSlipNumber": { "type": "string" },
"orderNumber": { "type": "string", "description": "Related purchase order or sales order number" },
"shipDate": { "type": "string", "description": "Ship date (YYYY-MM-DD)" },
"trackingNumber": { "type": "string" },
"recipient": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "type": "string" }
}
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"description": { "type": "string" },
"quantityOrdered": { "type": "number" },
"quantityShipped": { "type": "number" }
}
}
}
}
}
}'const { function: fn } = await client.functions.create({
functionName: "packing-slip-extractor",
type: "extract",
displayName: "Packing Slip Extractor",
tags: ["logistics", "fulfillment"],
outputSchemaName: "PackingSlip",
outputSchema: {
type: "object",
required: ["packingSlipNumber", "recipient"],
properties: {
packingSlipNumber: { type: "string" },
orderNumber: { type: "string", description: "Related purchase order or sales order number" },
shipDate: { type: "string", description: "Ship date (YYYY-MM-DD)" },
trackingNumber: { type: "string" },
recipient: {
type: "object",
properties: {
name: { type: "string" },
address: { type: "string" },
},
},
items: {
type: "array",
items: {
type: "object",
properties: {
sku: { type: "string" },
description: { type: "string" },
quantityOrdered: { type: "number" },
quantityShipped: { type: "number" },
},
},
},
},
},
});response = client.functions.create(
function_name="packing-slip-extractor",
type="extract",
display_name="Packing Slip Extractor",
tags=["logistics", "fulfillment"],
output_schema_name="PackingSlip",
output_schema={
"type": "object",
"required": ["packingSlipNumber", "recipient"],
"properties": {
"packingSlipNumber": {"type": "string"},
"orderNumber": {"type": "string", "description": "Related purchase order or sales order number"},
"shipDate": {"type": "string", "description": "Ship date (YYYY-MM-DD)"},
"trackingNumber": {"type": "string"},
"recipient": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"type": "string"},
},
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"description": {"type": "string"},
"quantityOrdered": {"type": "number"},
"quantityShipped": {"type": "number"},
},
},
},
},
},
)schema := map[string]any{
"type": "object",
"required": []string{"packingSlipNumber", "recipient"},
"properties": map[string]any{
"packingSlipNumber": map[string]any{"type": "string"},
"orderNumber": map[string]any{"type": "string"},
"shipDate": map[string]any{"type": "string"},
"trackingNumber": map[string]any{"type": "string"},
"recipient": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"address": map[string]any{"type": "string"},
},
},
"items": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"sku": map[string]any{"type": "string"},
"description": map[string]any{"type": "string"},
"quantityOrdered": map[string]any{"type": "number"},
"quantityShipped": map[string]any{"type": "number"},
},
},
},
},
}
resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
CreateFunction: bem.CreateFunctionUnionParam{
OfExtract: &bem.CreateFunctionExtractParam{
FunctionName: "packing-slip-extractor",
DisplayName: bem.String("Packing Slip Extractor"),
Tags: []string{"logistics", "fulfillment"},
OutputSchemaName: bem.String("PackingSlip"),
OutputSchema: schema,
},
},
})var schemaJson = """
{
"type": "object",
"required": ["packingSlipNumber", "recipient"],
"properties": {
"packingSlipNumber": { "type": "string" },
"orderNumber": { "type": "string", "description": "Related purchase order or sales order number" },
"shipDate": { "type": "string", "description": "Ship date (YYYY-MM-DD)" },
"trackingNumber": { "type": "string" },
"recipient": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"description": { "type": "string" },
"quantityOrdered": { "type": "number" },
"quantityShipped": { "type": "number" }
}
}
}
}
}
""";
var response = await client.Functions.Create(new FunctionCreateParams
{
CreateFunction = new Extract
{
FunctionName = "packing-slip-extractor",
DisplayName = "Packing Slip Extractor",
Tags = ["logistics", "fulfillment"],
OutputSchemaName = "PackingSlip",
OutputSchema = JsonSerializer.Deserialize<JsonElement>(schemaJson),
},
});bem functions create \
--function-name packing-slip-extractor \
--type extract \
--display-name "Packing Slip Extractor" \
--tags '["logistics", "fulfillment"]' \
--output-schema-name PackingSlip \
--output-schema '{
"type": "object",
"required": ["packingSlipNumber", "recipient"],
"properties": {
"packingSlipNumber": { "type": "string" },
"orderNumber": { "type": "string" },
"shipDate": { "type": "string" },
"trackingNumber": { "type": "string" },
"recipient": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "string" } } },
"items": { "type": "array", "items": { "type": "object", "properties": { "sku": { "type": "string" }, "description": { "type": "string" }, "quantityOrdered": { "type": "number" }, "quantityShipped": { "type": "number" } } } }
}
}'Step 4: Create the Classify function
The Classify function decides which extractor to route to. Each classifications[] entry has a name (used by the workflow's edge destinationName), a description the model uses to make the call, and a functionName pointing at the destination.
Write the descriptions like you'd write a routing rubric for an intern: include the distinguishing features, not the generic ones. "Has line items" is too weak — invoices, BOLs, and packing slips can all have line items. "Has unit prices and an amount due" is the discriminator for an invoice.
curl -X POST https://api.bem.ai/v3/functions \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"functionName": "logistics-classifier",
"type": "classify",
"displayName": "Logistics Document Classifier",
"tags": ["logistics"],
"description": "Classifies inbound logistics documents and routes each to the right extractor. Inputs are typically PDFs or scans of a single document.",
"classifications": [
{
"name": "invoice",
"functionName": "invoice-extractor",
"description": "An invoice or bill from a vendor requesting payment for goods or services. Distinguishing features: line items with unit prices and an amount due, a payment due date, vendor billing address. Common headers include INVOICE, BILL, STATEMENT."
},
{
"name": "bill_of_lading",
"functionName": "bol-extractor",
"description": "A bill of lading (BOL) — the freight contract between shipper and carrier. Distinguishing features: a BOL or pro number, named carrier, freight class per line, weight in pounds. Common headers include BILL OF LADING, STRAIGHT BILL OF LADING."
},
{
"name": "packing_slip",
"functionName": "packing-slip-extractor",
"description": "A packing slip listing what was physically shipped to a recipient. Distinguishing features: recipient address, line items with quantity ordered and quantity shipped, no prices or freight classes. Common headers include PACKING SLIP, PACKING LIST, SHIPMENT NOTICE."
}
]
}'const { function: fn } = await client.functions.create({
functionName: "logistics-classifier",
type: "classify",
displayName: "Logistics Document Classifier",
tags: ["logistics"],
description: "Classifies inbound logistics documents and routes each to the right extractor. Inputs are typically PDFs or scans of a single document.",
classifications: [
{
name: "invoice",
functionName: "invoice-extractor",
description: "An invoice or bill from a vendor requesting payment for goods or services. Distinguishing features: line items with unit prices and an amount due, a payment due date, vendor billing address. Common headers include INVOICE, BILL, STATEMENT.",
},
{
name: "bill_of_lading",
functionName: "bol-extractor",
description: "A bill of lading (BOL) — the freight contract between shipper and carrier. Distinguishing features: a BOL or pro number, named carrier, freight class per line, weight in pounds. Common headers include BILL OF LADING, STRAIGHT BILL OF LADING.",
},
{
name: "packing_slip",
functionName: "packing-slip-extractor",
description: "A packing slip listing what was physically shipped to a recipient. Distinguishing features: recipient address, line items with quantity ordered and quantity shipped, no prices or freight classes. Common headers include PACKING SLIP, PACKING LIST, SHIPMENT NOTICE.",
},
],
});response = client.functions.create(
function_name="logistics-classifier",
type="classify",
display_name="Logistics Document Classifier",
tags=["logistics"],
description="Classifies inbound logistics documents and routes each to the right extractor. Inputs are typically PDFs or scans of a single document.",
classifications=[
{
"name": "invoice",
"function_name": "invoice-extractor",
"description": "An invoice or bill from a vendor requesting payment for goods or services. Distinguishing features: line items with unit prices and an amount due, a payment due date, vendor billing address. Common headers include INVOICE, BILL, STATEMENT.",
},
{
"name": "bill_of_lading",
"function_name": "bol-extractor",
"description": "A bill of lading (BOL) — the freight contract between shipper and carrier. Distinguishing features: a BOL or pro number, named carrier, freight class per line, weight in pounds. Common headers include BILL OF LADING, STRAIGHT BILL OF LADING.",
},
{
"name": "packing_slip",
"function_name": "packing-slip-extractor",
"description": "A packing slip listing what was physically shipped to a recipient. Distinguishing features: recipient address, line items with quantity ordered and quantity shipped, no prices or freight classes. Common headers include PACKING SLIP, PACKING LIST, SHIPMENT NOTICE.",
},
],
)resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
CreateFunction: bem.CreateFunctionUnionParam{
OfClassify: &bem.CreateFunctionClassifyParam{
FunctionName: "logistics-classifier",
DisplayName: bem.String("Logistics Document Classifier"),
Tags: []string{"logistics"},
Description: bem.String("Classifies inbound logistics documents and routes each to the right extractor. Inputs are typically PDFs or scans of a single document."),
Classifications: []bem.ClassificationListItemParam{
{
Name: bem.String("invoice"),
FunctionName: bem.String("invoice-extractor"),
Description: bem.String("An invoice or bill from a vendor requesting payment for goods or services. Distinguishing features: line items with unit prices and an amount due, a payment due date, vendor billing address. Common headers include INVOICE, BILL, STATEMENT."),
},
{
Name: bem.String("bill_of_lading"),
FunctionName: bem.String("bol-extractor"),
Description: bem.String("A bill of lading (BOL) — the freight contract between shipper and carrier. Distinguishing features: a BOL or pro number, named carrier, freight class per line, weight in pounds. Common headers include BILL OF LADING, STRAIGHT BILL OF LADING."),
},
{
Name: bem.String("packing_slip"),
FunctionName: bem.String("packing-slip-extractor"),
Description: bem.String("A packing slip listing what was physically shipped to a recipient. Distinguishing features: recipient address, line items with quantity ordered and quantity shipped, no prices or freight classes. Common headers include PACKING SLIP, PACKING LIST, SHIPMENT NOTICE."),
},
},
},
},
})var response = await client.Functions.Create(new FunctionCreateParams
{
CreateFunction = new Classify
{
FunctionName = "logistics-classifier",
DisplayName = "Logistics Document Classifier",
Tags = ["logistics"],
Description = "Classifies inbound logistics documents and routes each to the right extractor. Inputs are typically PDFs or scans of a single document.",
Classifications = [
new ClassificationListItem
{
Name = "invoice",
FunctionName = "invoice-extractor",
Description = "An invoice or bill from a vendor requesting payment for goods or services. Distinguishing features: line items with unit prices and an amount due, a payment due date, vendor billing address. Common headers include INVOICE, BILL, STATEMENT.",
},
new ClassificationListItem
{
Name = "bill_of_lading",
FunctionName = "bol-extractor",
Description = "A bill of lading (BOL) — the freight contract between shipper and carrier. Distinguishing features: a BOL or pro number, named carrier, freight class per line, weight in pounds. Common headers include BILL OF LADING, STRAIGHT BILL OF LADING.",
},
new ClassificationListItem
{
Name = "packing_slip",
FunctionName = "packing-slip-extractor",
Description = "A packing slip listing what was physically shipped to a recipient. Distinguishing features: recipient address, line items with quantity ordered and quantity shipped, no prices or freight classes. Common headers include PACKING SLIP, PACKING LIST, SHIPMENT NOTICE.",
},
],
},
});bem functions create \
--function-name logistics-classifier \
--type classify \
--display-name "Logistics Document Classifier" \
--tags '["logistics"]' \
--description "Classifies inbound logistics documents and routes each to the right extractor. Inputs are typically PDFs or scans of a single document." \
--classification '[
{
"name": "invoice",
"functionName": "invoice-extractor",
"description": "An invoice or bill from a vendor requesting payment for goods or services. Distinguishing features: line items with unit prices and an amount due, a payment due date, vendor billing address."
},
{
"name": "bill_of_lading",
"functionName": "bol-extractor",
"description": "A bill of lading (BOL) — the freight contract between shipper and carrier. Distinguishing features: a BOL or pro number, named carrier, freight class per line, weight in pounds."
},
{
"name": "packing_slip",
"functionName": "packing-slip-extractor",
"description": "A packing slip listing what was physically shipped to a recipient. Distinguishing features: recipient address, line items with quantity ordered and quantity shipped, no prices or freight classes."
}
]'Step 5: Create the workflow
Now wire all four functions into a single workflow. The classifier is the entry point (mainNodeName); each edge leaves the classifier with a destinationName matching one of the classifications and arrives at the matching extractor.
+--> invoice-extractor (destinationName: "invoice")
|
Input --> doc-classifier -------+--> bol-extractor (destinationName: "bill_of_lading")
|
+--> packing-slip-extractor (destinationName: "packing_slip")curl -X POST https://api.bem.ai/v3/workflows \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"name": "logistics-triage",
"displayName": "Logistics Document Triage",
"tags": ["logistics"],
"mainNodeName": "doc-classifier",
"nodes": [
{ "name": "doc-classifier", "function": { "name": "logistics-classifier" } },
{ "name": "invoice-extractor", "function": { "name": "invoice-extractor" } },
{ "name": "bol-extractor", "function": { "name": "bol-extractor" } },
{ "name": "packing-slip-extractor", "function": { "name": "packing-slip-extractor" } }
],
"edges": [
{ "sourceNodeName": "doc-classifier", "destinationName": "invoice", "destinationNodeName": "invoice-extractor" },
{ "sourceNodeName": "doc-classifier", "destinationName": "bill_of_lading", "destinationNodeName": "bol-extractor" },
{ "sourceNodeName": "doc-classifier", "destinationName": "packing_slip", "destinationNodeName": "packing-slip-extractor" }
]
}'const { workflow } = await client.workflows.create({
name: "logistics-triage",
displayName: "Logistics Document Triage",
tags: ["logistics"],
mainNodeName: "doc-classifier",
nodes: [
{ name: "doc-classifier", function: { name: "logistics-classifier" } },
{ name: "invoice-extractor", function: { name: "invoice-extractor" } },
{ name: "bol-extractor", function: { name: "bol-extractor" } },
{ name: "packing-slip-extractor", function: { name: "packing-slip-extractor" } },
],
edges: [
{ sourceNodeName: "doc-classifier", destinationName: "invoice", destinationNodeName: "invoice-extractor" },
{ sourceNodeName: "doc-classifier", destinationName: "bill_of_lading", destinationNodeName: "bol-extractor" },
{ sourceNodeName: "doc-classifier", destinationName: "packing_slip", destinationNodeName: "packing-slip-extractor" },
],
});
console.log(workflow);response = client.workflows.create(
name="logistics-triage",
display_name="Logistics Document Triage",
tags=["logistics"],
main_node_name="doc-classifier",
nodes=[
{"name": "doc-classifier", "function": {"name": "logistics-classifier"}},
{"name": "invoice-extractor", "function": {"name": "invoice-extractor"}},
{"name": "bol-extractor", "function": {"name": "bol-extractor"}},
{"name": "packing-slip-extractor", "function": {"name": "packing-slip-extractor"}},
],
edges=[
{"source_node_name": "doc-classifier", "destination_name": "invoice", "destination_node_name": "invoice-extractor"},
{"source_node_name": "doc-classifier", "destination_name": "bill_of_lading", "destination_node_name": "bol-extractor"},
{"source_node_name": "doc-classifier", "destination_name": "packing_slip", "destination_node_name": "packing-slip-extractor"},
],
)
print(response.workflow)resp, err := client.Workflows.New(context.TODO(), bem.WorkflowNewParams{
Name: "logistics-triage",
DisplayName: bem.String("Logistics Document Triage"),
Tags: []string{"logistics"},
MainNodeName: "doc-classifier",
Nodes: []bem.WorkflowNewParamsNode{
{Name: bem.String("doc-classifier"), Function: bem.FunctionVersionIdentifierParam{Name: bem.String("logistics-classifier")}},
{Name: bem.String("invoice-extractor"), Function: bem.FunctionVersionIdentifierParam{Name: bem.String("invoice-extractor")}},
{Name: bem.String("bol-extractor"), Function: bem.FunctionVersionIdentifierParam{Name: bem.String("bol-extractor")}},
{Name: bem.String("packing-slip-extractor"), Function: bem.FunctionVersionIdentifierParam{Name: bem.String("packing-slip-extractor")}},
},
Edges: []bem.WorkflowNewParamsEdge{
{SourceNodeName: "doc-classifier", DestinationName: bem.String("invoice"), DestinationNodeName: "invoice-extractor"},
{SourceNodeName: "doc-classifier", DestinationName: bem.String("bill_of_lading"), DestinationNodeName: "bol-extractor"},
{SourceNodeName: "doc-classifier", DestinationName: bem.String("packing_slip"), DestinationNodeName: "packing-slip-extractor"},
},
})using Bem.Models.Workflows;
var response = await client.Workflows.Create(new WorkflowCreateParams
{
Name = "logistics-triage",
DisplayName = "Logistics Document Triage",
Tags = ["logistics"],
MainNodeName = "doc-classifier",
Nodes = [
new Node { Name = "doc-classifier", Function = new FunctionVersionIdentifier { Name = "logistics-classifier" } },
new Node { Name = "invoice-extractor", Function = new FunctionVersionIdentifier { Name = "invoice-extractor" } },
new Node { Name = "bol-extractor", Function = new FunctionVersionIdentifier { Name = "bol-extractor" } },
new Node { Name = "packing-slip-extractor", Function = new FunctionVersionIdentifier { Name = "packing-slip-extractor" } },
],
Edges = [
new Edge { SourceNodeName = "doc-classifier", DestinationName = "invoice", DestinationNodeName = "invoice-extractor" },
new Edge { SourceNodeName = "doc-classifier", DestinationName = "bill_of_lading", DestinationNodeName = "bol-extractor" },
new Edge { SourceNodeName = "doc-classifier", DestinationName = "packing_slip", DestinationNodeName = "packing-slip-extractor" },
],
});bem workflows create \
--name logistics-triage \
--display-name "Logistics Document Triage" \
--tags '["logistics"]' \
--main-node-name doc-classifier \
--nodes '[
{ "name": "doc-classifier", "function": { "name": "logistics-classifier" } },
{ "name": "invoice-extractor", "function": { "name": "invoice-extractor" } },
{ "name": "bol-extractor", "function": { "name": "bol-extractor" } },
{ "name": "packing-slip-extractor", "function": { "name": "packing-slip-extractor" } }
]' \
--edges '[
{ "sourceNodeName": "doc-classifier", "destinationName": "invoice", "destinationNodeName": "invoice-extractor" },
{ "sourceNodeName": "doc-classifier", "destinationName": "bill_of_lading", "destinationNodeName": "bol-extractor" },
{ "sourceNodeName": "doc-classifier", "destinationName": "packing_slip", "destinationNodeName": "packing-slip-extractor" }
]'Step 6: Call the workflow with a logistics document
Send a document through the triage. The example uses an invoice; swap the file for bill-of-lading.pdf or packing-slip.pdf to confirm the other branches route correctly.
Upload as multipart form data:
curl -X POST "https://api.bem.ai/v3/workflows/logistics-triage/call" \
-H "x-api-key: $BEM_API_KEY" \
-F "wait=true" \
-F "callReferenceID=invoice-001" \
-F "file=@invoice.pdf"Or, JSON body with base64-encoded file:
curl -X POST "https://api.bem.ai/v3/workflows/logistics-triage/call?wait=true" \
-H "Content-Type: application/json" \
-H "x-api-key: $BEM_API_KEY" \
-d '{
"callReferenceID": "invoice-001",
"input": {
"singleFile": {
"inputType": "pdf",
"inputContent": "'"$(base64 -i invoice.pdf)"'"
}
}
}'import fs from "node:fs";
const inputContent = fs.readFileSync("invoice.pdf").toString("base64");
const { call } = await client.workflows.call("logistics-triage", {
wait: true,
callReferenceID: "invoice-001",
input: {
singleFile: {
inputType: "pdf",
inputContent,
},
},
});
console.log(call?.status, call?.outputs);import base64
with open("invoice.pdf", "rb") as f:
input_content = base64.b64encode(f.read()).decode()
response = client.workflows.call(
"logistics-triage",
wait=True,
call_reference_id="invoice-001",
input={
"single_file": {
"input_type": "pdf",
"input_content": input_content,
}
},
)
print(response.call.status, response.call.outputs)data, err := os.ReadFile("invoice.pdf")
if err != nil {
panic(err)
}
encoded := base64.StdEncoding.EncodeToString(data)
resp, err := client.Workflows.Call(context.TODO(), "logistics-triage", bem.WorkflowCallParams{
Wait: bem.Bool(true),
CallReferenceID: bem.String("invoice-001"),
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("invoice.pdf");
var encoded = Convert.ToBase64String(bytes);
var response = await client.Workflows.Call("logistics-triage", new WorkflowCallParams
{
Wait = true,
CallReferenceID = "invoice-001",
Input = new Input
{
SingleFile = new SingleFile
{
InputType = "pdf",
InputContent = encoded,
},
},
});
Console.WriteLine(response.Call.Status);bem workflows call \
--workflow-name logistics-triage \
--wait \
--call-reference-id invoice-001 \
--input.single-file '{"inputContent": "@invoice.pdf", "inputType": "pdf"}'The @invoice.pdf syntax tells the CLI to read and base64-encode the file inline.
Response:
{
"call": {
"callID": "wc_abc123",
"callReferenceID": "invoice-001",
"status": "completed",
"workflowName": "logistics-triage",
"workflowVersionNum": 1,
"outputs": [
{
"eventID": "ev_…",
"eventType": "extract",
"functionName": "invoice-extractor",
"transformedContent": {
"invoiceNumber": "INV-2026-04188",
"invoiceDate": "2026-04-22",
"dueDate": "2026-05-22",
"vendor": {
"name": "Acme Logistics, Inc.",
"address": "1523 Pacific Ave, Santa Cruz, CA 95060"
},
"lineItems": [
{ "description": "LTL freight, San Jose → Phoenix", "quantity": 1, "unitPrice": 1248.00, "amount": 1248.00 },
{ "description": "Liftgate service", "quantity": 1, "unitPrice": 75.00, "amount": 75.00 }
],
"subtotal": 1323.00,
"totalAmount": 1323.00
}
}
],
"errors": [],
"url": "/v3/calls/wc_abc123",
"traceUrl": "/v3/calls/wc_abc123/trace"
}
}The terminal output is the extractor's output, not the classifier's. The classifier's decision is implicit in outputs[].functionName (invoice-extractor here, meaning the file was classified as an invoice). The extracted JSON lives at outputs[0].transformedContent — see Reading workflow call outputs for the per-event-type field map and accessor patterns. For full per-node visibility, fetch GET /v3/calls/{callID}/trace, which returns the complete execution graph including the classifier's chosen label.
Adding an error fallback
The classifier as built will fail a call when an inbound file matches none of the three classifications. To handle the long tail of mis-routed mail or off-template documents, add a fourth classification with isErrorFallback: true and route it to a generic extractor — usually one with a permissive schema:
{
"name": "unknown",
"functionName": "logistics-fallback-extractor",
"isErrorFallback": true,
"description": "Inbound document that doesn't match any of the known logistics types. Use as a catch-all for off-template forms or mis-routed mail."
}Then add the fallback extractor as a node in the workflow and an edge from doc-classifier with destinationName: "unknown". The Classify Functions reference covers the pattern in full.
Next steps
Classify Functions
The full reference for branching workflows: classifications, descriptions, error fallbacks
Extract Functions
Schema design, tabular chunking, and visual vs text-first inputs
Workflows Explained
Sequential, branching, splitting, joining — every DAG shape
Schema building guide
Designing outputSchema for reliable extraction