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

Hand off to an LLM

This cookbook builds an end-to-end logistics-document triage pipeline. By the end you will have:

  1. 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
  2. A Classify function that decides which document type an inbound file is
  3. A workflow that wires them into a branching DAG: one entry point, three terminal outputs
  4. 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_KEY exported 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

On this page