LLM-readable documentation index

Quickstart

Make your first synchronous workflow call with bem

Hand off to an LLM

This guide walks you through running your first bem workflow end to end. By the end you'll have:

  1. An extract function with a JSON schema describing the structure you want to pull out
  2. A workflow that wires that function into a reusable entry point
  3. A synchronous call that returns your structured data in one request (wait=true)

Pick a language from the tabs in each step — the flow is identical across cURL, the SDKs, and the CLI.

Prerequisites

  • A bem account (free to sign up)
  • An API key generated from Settings → API Keys in the bem UI
  • A document to run through the workflow — grab the sample invoice below

Sample invoice

Every example on this page runs against the same one-page PDF: a rental invoice from Appleseed Paint Co. Download it into your working directory so the invoice.pdf references below resolve:

curl -O https://docs.bem.ai/sample/invoice.pdf

Download invoice.pdf directly if you'd rather grab it from the browser.

The extracted values shown throughout this guide come from that file, so you can compare your output field by field. Bring your own invoice instead if you prefer — the schema in Step 3 is a starting point, not a fixed contract.

Step 1: Set your API key

Export your API key so the SDKs, CLI, and curl examples can pick it up from the environment:

export BEM_API_KEY='your-api-key-here'

To persist the key across shell sessions, add that line to your shell profile (such as ~/.zshrc or ~/.bashrc).

Step 2: Install the SDK

Skip this step if you're using curl.

curl ships with macOS and most Linux distributions — no install required.

npm install bem-ai-sdk
pip install bem-sdk
go get github.com/bem-team/bem-go-sdk
dotnet add package Bem
brew install bem-team/tools/bem

Or, with Go:

go install github.com/bem-team/bem-cli/cmd/bem@latest

Step 3: Create an extract function

Extract functions pull structured JSON out of unstructured files. The outputSchema below describes the fields we want to pull off the sample invoice — header details, the rental line items, and the totals. Customize it to match any document type.

The sample invoice states payment terms (NET 30) rather than an explicit due date, and prints an address for the bill-to party only, so the schema models those as paymentTerms and billTo instead of dueDate and vendor.address. Ask for fields your document actually carries: a field with nothing behind it comes back null.

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",
    "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)" },
        "paymentTerms":  { "type": "string", "description": "Payment terms as printed, e.g. NET 30" },
        "vendor": {
          "type": "object",
          "properties": {
            "name": { "type": "string", "description": "Company issuing the invoice" }
          }
        },
        "billTo": {
          "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", "description": "Rate charged per unit" },
              "rateUnit":    { "type": "string", "description": "Unit the rate is charged against, e.g. day, week, panel" },
              "amount":      { "type": "number" }
            }
          }
        },
        "subtotal":    { "type": "number" },
        "taxAmount":   { "type": "number" },
        "totalAmount": { "type": "number" }
      }
    }
  }'

Save as create-function.ts:

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",
  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)" },
      paymentTerms:  { type: "string", description: "Payment terms as printed, e.g. NET 30" },
      vendor: {
        type: "object",
        properties: {
          name: { type: "string", description: "Company issuing the invoice" },
        },
      },
      billTo: {
        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", description: "Rate charged per unit" },
            rateUnit:    { type: "string", description: "Unit the rate is charged against, e.g. day, week, panel" },
            amount:      { type: "number" },
          },
        },
      },
      subtotal:    { type: "number" },
      taxAmount:   { type: "number" },
      totalAmount: { type: "number" },
    },
  },
});

console.log(fn);

Run it with npx tsx create-function.ts.

Save as create_function.py:

from bem import Bem

client = Bem()

response = client.functions.create(
    function_name="invoice-extractor",
    type="extract",
    display_name="Invoice Extractor",
    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)"},
            "paymentTerms":  {"type": "string", "description": "Payment terms as printed, e.g. NET 30"},
            "vendor": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Company issuing the invoice"},
                },
            },
            "billTo": {
                "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", "description": "Rate charged per unit"},
                        "rateUnit":    {"type": "string", "description": "Unit the rate is charged against, e.g. day, week, panel"},
                        "amount":      {"type": "number"},
                    },
                },
            },
            "subtotal":    {"type": "number"},
            "taxAmount":   {"type": "number"},
            "totalAmount": {"type": "number"},
        },
    },
)
print(response.function)

Run it with python create_function.py.

Save as create_function.go:

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"},
            "paymentTerms":  map[string]any{"type": "string"},
            "vendor": map[string]any{
                "type": "object",
                "properties": map[string]any{
                    "name": map[string]any{"type": "string"},
                },
            },
            "billTo": 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"},
                        "rateUnit":    map[string]any{"type": "string"},
                        "amount":      map[string]any{"type": "number"},
                    },
                },
            },
            "subtotal":    map[string]any{"type": "number"},
            "taxAmount":   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"),
                OutputSchemaName: bem.String("Invoice"),
                OutputSchema:     schema,
            },
        },
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", resp.Function)
}

Run it with go run create_function.go.

Save as CreateFunction.cs:

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)" },
    "paymentTerms":  { "type": "string", "description": "Payment terms as printed, e.g. NET 30" },
    "vendor": {
      "type": "object",
      "properties": {
        "name": { "type": "string", "description": "Company issuing the invoice" }
      }
    },
    "billTo": {
      "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", "description": "Rate charged per unit" },
          "rateUnit":    { "type": "string", "description": "Unit the rate is charged against, e.g. day, week, panel" },
          "amount":      { "type": "number" }
        }
      }
    },
    "subtotal":    { "type": "number" },
    "taxAmount":   { "type": "number" },
    "totalAmount": { "type": "number" }
  }
}
""";

var response = await client.Functions.Create(new FunctionCreateParams
{
    CreateFunction = new Extract
    {
        FunctionName     = "invoice-extractor",
        DisplayName      = "Invoice Extractor",
        OutputSchemaName = "Invoice",
        OutputSchema     = JsonSerializer.Deserialize<JsonElement>(schemaJson),
    },
});

Console.WriteLine(response.Function);

Run it from the project directory with dotnet run.

Put the schema in invoice-schema.json:

{
  "type": "object",
  "required": ["invoiceNumber", "vendor", "totalAmount"],
  "properties": {
    "invoiceNumber": { "type": "string" },
    "invoiceDate":   { "type": "string" },
    "paymentTerms":  { "type": "string" },
    "vendor": {
      "type": "object",
      "properties": {
        "name": { "type": "string" }
      }
    },
    "billTo": {
      "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" },
          "rateUnit":    { "type": "string" },
          "amount":      { "type": "number" }
        }
      }
    },
    "subtotal":    { "type": "number" },
    "taxAmount":   { "type": "number" },
    "totalAmount": { "type": "number" }
  }
}

Then create the function:

bem functions create \
  --function-name invoice-extractor \
  --type extract \
  --display-name "Invoice Extractor" \
  --output-schema-name Invoice \
  --output-schema @invoice-schema.json

Response:

{
  "function": {
    "functionID": "fn_2abc123xyz",
    "functionName": "invoice-extractor",
    "displayName": "Invoice Extractor",
    "type": "extract",
    "currentVersionNum": 1
  }
}
{
  functionID: 'fn_2abc123xyz',
  functionName: 'invoice-extractor',
  displayName: 'Invoice Extractor',
  type: 'extract',
  currentVersionNum: 1
}
Function(
    function_id='fn_2abc123xyz',
    function_name='invoice-extractor',
    display_name='Invoice Extractor',
    type='extract',
    current_version_num=1,
)
bem.Function{
    FunctionID:        "fn_2abc123xyz",
    FunctionName:      "invoice-extractor",
    DisplayName:       "Invoice Extractor",
    Type:              "extract",
    CurrentVersionNum: 1,
}
Bem.Models.Functions.Function {
  FunctionID        = "fn_2abc123xyz",
  FunctionName      = "invoice-extractor",
  DisplayName       = "Invoice Extractor",
  Type              = "extract",
  CurrentVersionNum = 1
}
{
  "function": {
    "functionID": "fn_2abc123xyz",
    "functionName": "invoice-extractor",
    "displayName": "Invoice Extractor",
    "type": "extract",
    "currentVersionNum": 1
  }
}

Step 4: Create a workflow

A workflow wires one or more functions into a reusable entry point. For a single-function pipeline you only need one node and no edges.

curl -X POST https://api.bem.ai/v3/workflows \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BEM_API_KEY" \
  -d '{
    "name": "invoice-processing",
    "displayName": "Invoice Processing Workflow",
    "tags": ["invoices", "financial-data"],
    "mainNodeName": "invoice-extractor",
    "nodes": [
      {
        "name": "invoice-extractor",
        "function": { "name": "invoice-extractor" }
      }
    ]
  }'
import Bem from "bem-ai-sdk";

const client = new Bem();

const { workflow } = await client.workflows.create({
  name: "invoice-processing",
  displayName: "Invoice Processing Workflow",
  tags: ["invoices", "financial-data"],
  mainNodeName: "invoice-extractor",
  nodes: [
    {
      name: "invoice-extractor",
      function: { name: "invoice-extractor" },
    },
  ],
});

console.log(workflow);
from bem import Bem

client = Bem()

response = client.workflows.create(
    name="invoice-processing",
    display_name="Invoice Processing Workflow",
    tags=["invoices", "financial-data"],
    main_node_name="invoice-extractor",
    nodes=[
        {
            "name": "invoice-extractor",
            "function": {"name": "invoice-extractor"},
        }
    ],
)
print(response.workflow)
package main

import (
    "context"
    "fmt"

    bem "github.com/bem-team/bem-go-sdk"
)

func main() {
    client := bem.NewClient()

    resp, err := client.Workflows.New(context.TODO(), bem.WorkflowNewParams{
        Name:         "invoice-processing",
        DisplayName:  bem.String("Invoice Processing Workflow"),
        Tags:         []string{"invoices", "financial-data"},
        MainNodeName: "invoice-extractor",
        Nodes: []bem.WorkflowNewParamsNode{
            {
                Name: bem.String("invoice-extractor"),
                Function: bem.FunctionVersionIdentifierParam{
                    Name: bem.String("invoice-extractor"),
                },
            },
        },
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", resp.Workflow)
}
using Bem;
using Bem.Models.Workflows;

BemClient client = new();

var response = await client.Workflows.Create(new WorkflowCreateParams
{
    Name         = "invoice-processing",
    DisplayName  = "Invoice Processing Workflow",
    Tags         = new List<string> { "invoices", "financial-data" },
    MainNodeName = "invoice-extractor",
    Nodes = new List<Node>
    {
        new Node
        {
            Function = new FunctionVersionIdentifier { Name = "invoice-extractor" },
            Name     = "invoice-extractor",
        },
    },
});

Console.WriteLine(response.Workflow);
bem workflows create \
  --name invoice-processing \
  --display-name "Invoice Processing Workflow" \
  --tags '["invoices", "financial-data"]' \
  --main-node-name invoice-extractor \
  --node '{name: invoice-extractor, function: {name: invoice-extractor}}'

Response:

{
  "workflow": {
    "id": "wf_2def456abc",
    "name": "invoice-processing",
    "displayName": "Invoice Processing Workflow",
    "versionNum": 1,
    "mainNodeName": "invoice-extractor",
    "nodes": [
      {
        "name": "invoice-extractor",
        "function": {
          "name": "invoice-extractor",
          "versionNum": 1
        }
      }
    ],
    "edges": []
  }
}
{
  id: 'wf_2def456abc',
  name: 'invoice-processing',
  displayName: 'Invoice Processing Workflow',
  versionNum: 1,
  mainNodeName: 'invoice-extractor',
  nodes: [
    {
      name: 'invoice-extractor',
      function: { name: 'invoice-extractor', versionNum: 1 }
    }
  ],
  edges: []
}
Workflow(
    id='wf_2def456abc',
    name='invoice-processing',
    display_name='Invoice Processing Workflow',
    version_num=1,
    main_node_name='invoice-extractor',
    nodes=[
        WorkflowNodeResponse(
            name='invoice-extractor',
            function=FunctionVersionIdentifier(name='invoice-extractor', version_num=1),
        )
    ],
    edges=[],
)
bem.Workflow{
    ID:           "wf_2def456abc",
    Name:         "invoice-processing",
    DisplayName:  "Invoice Processing Workflow",
    VersionNum:   1,
    MainNodeName: "invoice-extractor",
    Nodes: []bem.WorkflowNodeResponse{
        {
            Name: "invoice-extractor",
            Function: bem.FunctionVersionIdentifier{
                Name:       "invoice-extractor",
                VersionNum: 1,
            },
        },
    },
    Edges: []bem.WorkflowEdgeResponse{},
}
Bem.Models.Workflows.Workflow {
  ID           = "wf_2def456abc",
  Name         = "invoice-processing",
  DisplayName  = "Invoice Processing Workflow",
  VersionNum   = 1,
  MainNodeName = "invoice-extractor",
  Nodes        = [
    WorkflowNodeResponse {
      Name     = "invoice-extractor",
      Function = FunctionVersionIdentifier { Name = "invoice-extractor", VersionNum = 1 }
    }
  ],
  Edges        = []
}
{
  "workflow": {
    "id": "wf_2def456abc",
    "name": "invoice-processing",
    "displayName": "Invoice Processing Workflow",
    "versionNum": 1,
    "mainNodeName": "invoice-extractor",
    "nodes": [
      {
        "name": "invoice-extractor",
        "function": {
          "name": "invoice-extractor",
          "versionNum": 1
        }
      }
    ],
    "edges": []
  }
}

Step 5: Call the workflow synchronously

Pass wait=true (query param for JSON, form field for multipart) to block until the call finishes and return the completed result in the same response. The endpoint waits up to 30 seconds — longer-running calls still return a pending call object that you can poll or subscribe to.

JSON body with base64-encoded file:

curl -X POST "https://api.bem.ai/v3/workflows/invoice-processing/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)"'"
      }
    }
  }'

Or, upload the file as multipart form data:

curl -X POST "https://api.bem.ai/v3/workflows/invoice-processing/call" \
  -H "x-api-key: $BEM_API_KEY" \
  -F "wait=true" \
  -F "callReferenceID=invoice-001" \
  -F "file=@invoice.pdf"
import fs from "node:fs";
import Bem from "bem-ai-sdk";

const client = new Bem();

const inputContent = fs.readFileSync("invoice.pdf").toString("base64");

const { call } = await client.workflows.call("invoice-processing", {
  wait: true,
  callReferenceID: "invoice-001",
  input: {
    singleFile: {
      inputType: "pdf",
      inputContent,
    },
  },
});

console.log(call?.status);
console.log(call?.outputs);
import base64
from bem import Bem

client = Bem()

with open("invoice.pdf", "rb") as f:
    input_content = base64.b64encode(f.read()).decode()

response = client.workflows.call(
    "invoice-processing",
    wait=True,
    call_reference_id="invoice-001",
    input={
        "single_file": {
            "input_type": "pdf",
            "input_content": input_content,
        }
    },
)

print(response.call.status)
print(response.call.outputs)
package main

import (
    "context"
    "encoding/base64"
    "fmt"
    "os"

    bem "github.com/bem-team/bem-go-sdk"
)

func main() {
    client := bem.NewClient()

    data, err := os.ReadFile("invoice.pdf")
    if err != nil {
        panic(err)
    }
    encoded := base64.StdEncoding.EncodeToString(data)

    resp, err := client.Workflows.Call(context.TODO(), "invoice-processing", 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))
}
using Bem;
using Bem.Models.Workflows;

BemClient client = new();

var fileBytes = await File.ReadAllBytesAsync("invoice.pdf");
var inputContent = Convert.ToBase64String(fileBytes);

var response = await client.Workflows.Call("invoice-processing", new WorkflowCallParams
{
    Wait            = true,
    CallReferenceID = "invoice-001",
    Input = new Input
    {
        SingleFile = new FileInput
        {
            InputType    = InputType.Pdf,
            InputContent = inputContent,
        },
    },
});

Console.WriteLine($"status={response.Call.Status} outputs={response.Call.Outputs.Count}");

The CLI's @path/to/file syntax reads and base64-encodes the file for you.

bem workflows call \
  --workflow-name invoice-processing \
  --call-reference-id invoice-001 \
  --input.single-file '{"inputContent": "@invoice.pdf", "inputType": "pdf"}' \
  --wait

Important: --wait is a boolean flag. Use --wait or --wait=true — not --wait true with a space.

Response when the call finishes in time (HTTP 200):

{
  "call": {
    "callID": "wc_2ghi789def",
    "status": "completed",
    "workflowName": "invoice-processing",
    "workflowVersionNum": 1,
    "callReferenceID": "invoice-001",
    "createdAt": "2024-01-15T10:30:00Z",
    "finishedAt": "2024-01-15T10:30:12Z",
    "outputs": [
      {
        "eventID": "evt_3AlB5PRZFCwAxrwHziH9PQkbkav",
        "eventType": "transform",
        "transformedContent": {
          "invoiceNumber": "FE-2421 C",
          "invoiceDate": "2025-07-14",
          "paymentTerms": "NET 30",
          "vendor": {
            "name": "Appleseed Paint Co."
          },
          "billTo": {
            "name": "Pacific Stageworks",
            "address": "1129 Manna Blvd, San Diego, CA 92101"
          },
          "lineItems": [
            { "description": "Temporary Fence Panel, 6ft x 12ft",      "quantity": 40, "unitPrice": 22.0, "rateUnit": "panel", "amount": 880.0 },
            { "description": "Paint Sprayer Kit, HVLP Pro",            "quantity": 3,  "unitPrice": 95.0, "rateUnit": "day",   "amount": 285.0 },
            { "description": "Extension Ladder, 24ft Aluminum",        "quantity": 6,  "unitPrice": 18.0, "rateUnit": "day",   "amount": 108.0 },
            { "description": "Portable Generator, 7000W Quiet Series", "quantity": 2,  "unitPrice": 12.0, "rateUnit": "day",   "amount": 240.0 },
            { "description": "High-Visibility Job Site Lighting Kit",  "quantity": 4,  "unitPrice": 75.0, "rateUnit": "day",   "amount": 300.0 },
            { "description": "Paint Containment Tent, Mobile",         "quantity": 1,  "unitPrice": 65.0, "rateUnit": "week",  "amount": 650.0 }
          ],
          "subtotal": 2463.0,
          "taxAmount": 215.51,
          "totalAmount": 2678.51
        }
      }
    ],
    "errors": [],
    "url": "/v3/calls/wc_2ghi789def",
    "traceUrl": "/v3/calls/wc_2ghi789def/trace"
  }
}
{
  callID: 'wc_2ghi789def',
  status: 'completed',
  workflowName: 'invoice-processing',
  workflowVersionNum: 1,
  callReferenceID: 'invoice-001',
  createdAt: '2024-01-15T10:30:00Z',
  finishedAt: '2024-01-15T10:30:12Z',
  outputs: [
    {
      eventID: 'evt_3AlB5PRZFCwAxrwHziH9PQkbkav',
      eventType: 'transform',
      transformedContent: {
        invoiceNumber: 'FE-2421 C',
        invoiceDate: '2025-07-14',
        paymentTerms: 'NET 30',
        vendor: { name: 'Appleseed Paint Co.' },
        billTo: { name: 'Pacific Stageworks', address: '1129 Manna Blvd, San Diego, CA 92101' },
        lineItems: [
          { description: 'Temporary Fence Panel, 6ft x 12ft', quantity: 40, unitPrice: 22, rateUnit: 'panel', amount: 880 },
          { description: 'Paint Sprayer Kit, HVLP Pro', quantity: 3, unitPrice: 95, rateUnit: 'day', amount: 285 },
          { description: 'Extension Ladder, 24ft Aluminum', quantity: 6, unitPrice: 18, rateUnit: 'day', amount: 108 },
          { description: 'Portable Generator, 7000W Quiet Series', quantity: 2, unitPrice: 12, rateUnit: 'day', amount: 240 },
          { description: 'High-Visibility Job Site Lighting Kit', quantity: 4, unitPrice: 75, rateUnit: 'day', amount: 300 },
          { description: 'Paint Containment Tent, Mobile', quantity: 1, unitPrice: 65, rateUnit: 'week', amount: 650 }
        ],
        subtotal: 2463,
        taxAmount: 215.51,
        totalAmount: 2678.51
      }
    }
  ],
  errors: [],
  url: '/v3/calls/wc_2ghi789def',
  traceUrl: '/v3/calls/wc_2ghi789def/trace'
}
CallV3(
    call_id='wc_2ghi789def',
    status='completed',
    workflow_name='invoice-processing',
    workflow_version_num=1,
    call_reference_id='invoice-001',
    created_at=datetime.datetime(2024, 1, 15, 10, 30, tzinfo=tzutc()),
    finished_at=datetime.datetime(2024, 1, 15, 10, 30, 12, tzinfo=tzutc()),
    outputs=[
        Event(
            event_id='evt_3AlB5PRZFCwAxrwHziH9PQkbkav',
            event_type='transform',
            transformed_content={
                'invoiceNumber': 'FE-2421 C',
                'invoiceDate': '2025-07-14',
                'paymentTerms': 'NET 30',
                'vendor': {'name': 'Appleseed Paint Co.'},
                'billTo': {'name': 'Pacific Stageworks', 'address': '...'},
                'lineItems': [...],
                'subtotal': 2463.0,
                'taxAmount': 215.51,
                'totalAmount': 2678.51,
            },
        )
    ],
    errors=[],
    url='/v3/calls/wc_2ghi789def',
    trace_url='/v3/calls/wc_2ghi789def/trace',
)
bem.CallV3{
    CallID:             "wc_2ghi789def",
    Status:             "completed",
    WorkflowName:       "invoice-processing",
    WorkflowVersionNum: 1,
    CallReferenceID:    "invoice-001",
    CreatedAt:          time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC),
    FinishedAt:         time.Date(2024, 1, 15, 10, 30, 12, 0, time.UTC),
    Outputs: []bem.Event{
        {
            EventID:   "evt_3AlB5PRZFCwAxrwHziH9PQkbkav",
            EventType: "transform",
            TransformedContent: map[string]any{
                "invoiceNumber": "FE-2421 C",
                "invoiceDate":   "2025-07-14",
                "paymentTerms":  "NET 30",
                "vendor": map[string]any{
                    "name": "Appleseed Paint Co.",
                },
                "billTo": map[string]any{
                    "name":    "Pacific Stageworks",
                    "address": "1129 Manna Blvd, San Diego, CA 92101",
                },
                "lineItems": []any{
                    map[string]any{"description": "Temporary Fence Panel, 6ft x 12ft", "quantity": 40, "unitPrice": 22.0, "rateUnit": "panel", "amount": 880.0},
                    map[string]any{"description": "Paint Sprayer Kit, HVLP Pro", "quantity": 3, "unitPrice": 95.0, "rateUnit": "day", "amount": 285.0},
                    map[string]any{"description": "Extension Ladder, 24ft Aluminum", "quantity": 6, "unitPrice": 18.0, "rateUnit": "day", "amount": 108.0},
                    map[string]any{"description": "Portable Generator, 7000W Quiet Series", "quantity": 2, "unitPrice": 12.0, "rateUnit": "day", "amount": 240.0},
                    map[string]any{"description": "High-Visibility Job Site Lighting Kit", "quantity": 4, "unitPrice": 75.0, "rateUnit": "day", "amount": 300.0},
                    map[string]any{"description": "Paint Containment Tent, Mobile", "quantity": 1, "unitPrice": 65.0, "rateUnit": "week", "amount": 650.0},
                },
                "subtotal":    2463.0,
                "taxAmount":   215.51,
                "totalAmount": 2678.51,
            },
        },
    },
    Errors:   []bem.ErrorEvent{},
    URL:      "/v3/calls/wc_2ghi789def",
    TraceURL: "/v3/calls/wc_2ghi789def/trace",
}
Bem.Models.Calls.CallV3 {
  CallID             = "wc_2ghi789def",
  Status             = "completed",
  WorkflowName       = "invoice-processing",
  WorkflowVersionNum = 1,
  CallReferenceID    = "invoice-001",
  CreatedAt          = 2024-01-15T10:30:00Z,
  FinishedAt         = 2024-01-15T10:30:12Z,
  Outputs = [
    Event {
      EventID   = "evt_3AlB5PRZFCwAxrwHziH9PQkbkav",
      EventType = "transform",
      TransformedContent = {
        "invoiceNumber": "FE-2421 C",
        "invoiceDate":   "2025-07-14",
        "paymentTerms":  "NET 30",
        "vendor":        { "name": "Appleseed Paint Co." },
        "billTo":        { "name": "Pacific Stageworks", "address": "..." },
        "lineItems":     [ /* ... */ ],
        "subtotal":      2463.0,
        "taxAmount":     215.51,
        "totalAmount":   2678.51
      }
    }
  ],
  Errors   = [],
  Url      = "/v3/calls/wc_2ghi789def",
  TraceUrl = "/v3/calls/wc_2ghi789def/trace"
}
{
  "call": {
    "callID": "wc_2ghi789def",
    "status": "completed",
    "workflowName": "invoice-processing",
    "workflowVersionNum": 1,
    "callReferenceID": "invoice-001",
    "createdAt": "2024-01-15T10:30:00Z",
    "finishedAt": "2024-01-15T10:30:12Z",
    "outputs": [
      {
        "eventID": "evt_3AlB5PRZFCwAxrwHziH9PQkbkav",
        "eventType": "extract",
        "transformedContent": {
          "invoiceNumber": "FE-2421 C",
          "invoiceDate": "2025-07-14",
          "paymentTerms": "NET 30",
          "vendor": {
            "name": "Appleseed Paint Co."
          },
          "billTo": {
            "name": "Pacific Stageworks",
            "address": "1129 Manna Blvd, San Diego, CA 92101"
          },
          "lineItems": [
            { "description": "Temporary Fence Panel, 6ft x 12ft",      "quantity": 40, "unitPrice": 22.0, "rateUnit": "panel", "amount": 880.0 },
            { "description": "Paint Sprayer Kit, HVLP Pro",            "quantity": 3,  "unitPrice": 95.0, "rateUnit": "day",   "amount": 285.0 },
            { "description": "Extension Ladder, 24ft Aluminum",        "quantity": 6,  "unitPrice": 18.0, "rateUnit": "day",   "amount": 108.0 },
            { "description": "Portable Generator, 7000W Quiet Series", "quantity": 2,  "unitPrice": 12.0, "rateUnit": "day",   "amount": 240.0 },
            { "description": "High-Visibility Job Site Lighting Kit",  "quantity": 4,  "unitPrice": 75.0, "rateUnit": "day",   "amount": 300.0 },
            { "description": "Paint Containment Tent, Mobile",         "quantity": 1,  "unitPrice": 65.0, "rateUnit": "week",  "amount": 650.0 }
          ],
          "subtotal": 2463.0,
          "taxAmount": 215.51,
          "totalAmount": 2678.51
        }
      }
    ],
    "errors": [],
    "url": "/v3/calls/wc_2ghi789def",
    "traceUrl": "/v3/calls/wc_2ghi789def/trace"
  }
}

Tip: pipe through jq '.call.outputs[0].transformedContent' to extract just the extracted fields.

Reading the response

The extracted data lives at call.outputs[0].transformedContent — note the plural outputs[] (it's always an array, even with one terminal node) and the top-level transformedContent (the payload is on the event itself, not nested under a transformation field). For workflows where the terminal node is an Enrich function, the payload is at enrichedContent instead; the field name is keyed off eventType.

# With jq
curl ... | jq '.call.outputs[0].transformedContent'
const { call } = await client.workflows.call("invoice-processing", { wait: true, /* ... */ });
const data = call?.outputs?.[0]?.transformedContent;
console.log(data?.invoiceNumber, data?.totalAmount);
response = client.workflows.call("invoice-processing", wait=True, ...)
data = response.call.outputs[0].transformed_content
print(data["invoiceNumber"], data["totalAmount"])
resp, err := client.Workflows.Call(ctx, "invoice-processing", bem.WorkflowCallParams{Wait: bem.Bool(true) /* ... */})
if err != nil { panic(err) }
data := resp.Call.Outputs[0].TransformedContent
fmt.Printf("%+v\n", data)
var response = await client.Workflows.Call("invoice-processing", new WorkflowCallParams { Wait = true /* ... */ });
var data = response.Call.Outputs[0].TransformedContent;
Console.WriteLine(data);
bem workflows call --workflow-name invoice-processing --wait \
  --call-reference-id invoice-001 \
  --input.single-file '{"inputContent": "@invoice.pdf", "inputType": "pdf"}' \
  --transform 'call.outputs.0.transformedContent'

The --transform flag uses GJSON syntax so the CLI prints just the extracted JSON.

For the full per-event-type field map, the outputs[] array semantics, and accessor patterns when a workflow has multiple terminal nodes (branching/splitting), see Reading workflow call outputs.

If the call takes longer than 30 seconds, the endpoint returns HTTP 202 with status: "pending" or "running". When that happens, either poll GET /v3/calls/{callID} or configure a webhook subscription to receive the result asynchronously.

Expanding your workflow

Workflows are easy to iterate on — you can keep layering functions into the DAG as your pipeline grows. To show how that works, let's add an Enrich step that matches each extracted line-item description to a SKU in a product catalog.

We'll:

  1. Create a collection to hold the catalog
  2. Populate it with a handful of products
  3. Create an enrich function that does a semantic lookup against the collection
  4. Wire the enrich function into invoice-processing so it runs after invoice-extractor

Step 6a: Create a product catalog collection

curl -X POST https://api.bem.ai/v3/collections \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BEM_API_KEY" \
  -d '{"collectionName": "product_catalog"}'
import Bem from "bem-ai-sdk";

const client = new Bem();

const collection = await client.collections.create({
  collectionName: "product_catalog",
});

console.log(collection);
from bem import Bem

client = Bem()

collection = client.collections.create(
    collection_name="product_catalog",
)
print(collection)
package main

import (
    "context"
    "fmt"

    bem "github.com/bem-team/bem-go-sdk"
)

func main() {
    client := bem.NewClient()

    collection, err := client.Collections.New(context.TODO(), bem.CollectionNewParams{
        CollectionName: "product_catalog",
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", collection)
}
using Bem;
using Bem.Models.Collections;

BemClient client = new();

var collection = await client.Collections.Create(new CollectionCreateParams
{
    CollectionName = "product_catalog",
});

Console.WriteLine(collection);
bem collections create --collection-name product_catalog

Response:

{
  "collectionID": "cl_2N6gH8ZKCmvb6BnFcGqhKJ98VzP",
  "collectionName": "product_catalog",
  "itemCount": 0,
  "createdAt": "2026-04-22T15:30:00Z",
  "updatedAt": "2026-04-22T15:30:00Z"
}
{
  collectionID: 'cl_2N6gH8ZKCmvb6BnFcGqhKJ98VzP',
  collectionName: 'product_catalog',
  itemCount: 0,
  createdAt: '2026-04-22T15:30:00Z',
  updatedAt: '2026-04-22T15:30:00Z'
}
CollectionCreateResponse(
    collection_id='cl_2N6gH8ZKCmvb6BnFcGqhKJ98VzP',
    collection_name='product_catalog',
    item_count=0,
    created_at=datetime.datetime(2026, 4, 22, 15, 30, tzinfo=tzutc()),
    updated_at=datetime.datetime(2026, 4, 22, 15, 30, tzinfo=tzutc()),
)
&bem.CollectionNewResponse{
    CollectionID:   "cl_2N6gH8ZKCmvb6BnFcGqhKJ98VzP",
    CollectionName: "product_catalog",
    ItemCount:      0,
    CreatedAt:      time.Date(2026, 4, 22, 15, 30, 0, 0, time.UTC),
    UpdatedAt:      time.Date(2026, 4, 22, 15, 30, 0, 0, time.UTC),
}
Bem.Models.Collections.CollectionCreateResponse {
  CollectionID   = "cl_2N6gH8ZKCmvb6BnFcGqhKJ98VzP",
  CollectionName = "product_catalog",
  ItemCount      = 0,
  CreatedAt      = 2026-04-22T15:30:00Z,
  UpdatedAt      = 2026-04-22T15:30:00Z
}
{
  "collectionID": "cl_2N6gH8ZKCmvb6BnFcGqhKJ98VzP",
  "collectionName": "product_catalog",
  "itemCount": 0,
  "createdAt": "2026-04-22T15:30:00Z",
  "updatedAt": "2026-04-22T15:30:00Z"
}

Step 6b: Add products to the collection

Each item's data can be a plain string or a JSON object. Objects let you retrieve structured fields (SKU, unit cost, category) when a match is found.

curl -X POST https://api.bem.ai/v3/collections/items \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BEM_API_KEY" \
  -d '{
    "collectionName": "product_catalog",
    "items": [
      { "data": { "sku": "FENCE-6X12-TMP",   "name": "Temporary Fence Panel 6ft x 12ft",     "category": "Site Safety",     "unitCost":  24.00 } },
      { "data": { "sku": "SPRAY-HVLP-PRO",   "name": "HVLP Pro Paint Sprayer Kit",           "category": "Paint Equipment", "unitCost": 110.00 } },
      { "data": { "sku": "LADDER-EXT-24AL",  "name": "Extension Ladder 24ft Aluminum",       "category": "Access",          "unitCost":  20.00 } },
      { "data": { "sku": "GEN-7000W-QS",     "name": "Portable Generator 7000W Quiet Series","category": "Power",           "unitCost": 135.00 } },
      { "data": { "sku": "LIGHT-JOBSITE-HV", "name": "High-Visibility Job Site Lighting Kit","category": "Lighting",        "unitCost":  85.00 } },
      { "data": { "sku": "TENT-CONTAIN-MOB", "name": "Mobile Paint Containment Tent",        "category": "Paint Equipment", "unitCost": 700.00 } }
    ]
  }'
import Bem from "bem-ai-sdk";

const client = new Bem();

const response = await client.collections.items.add({
  collectionName: "product_catalog",
  items: [
    { data: { sku: "FENCE-6X12-TMP",   name: "Temporary Fence Panel 6ft x 12ft",      category: "Site Safety",     unitCost:  24.00 } },
    { data: { sku: "SPRAY-HVLP-PRO",   name: "HVLP Pro Paint Sprayer Kit",            category: "Paint Equipment", unitCost: 110.00 } },
    { data: { sku: "LADDER-EXT-24AL",  name: "Extension Ladder 24ft Aluminum",        category: "Access",          unitCost:  20.00 } },
    { data: { sku: "GEN-7000W-QS",     name: "Portable Generator 7000W Quiet Series", category: "Power",           unitCost: 135.00 } },
    { data: { sku: "LIGHT-JOBSITE-HV", name: "High-Visibility Job Site Lighting Kit", category: "Lighting",        unitCost:  85.00 } },
    { data: { sku: "TENT-CONTAIN-MOB", name: "Mobile Paint Containment Tent",         category: "Paint Equipment", unitCost: 700.00 } },
  ],
});

console.log(response);
from bem import Bem

client = Bem()

response = client.collections.items.add(
    collection_name="product_catalog",
    items=[
        {"data": {"sku": "FENCE-6X12-TMP",   "name": "Temporary Fence Panel 6ft x 12ft",      "category": "Site Safety",     "unitCost":  24.00}},
        {"data": {"sku": "SPRAY-HVLP-PRO",   "name": "HVLP Pro Paint Sprayer Kit",            "category": "Paint Equipment", "unitCost": 110.00}},
        {"data": {"sku": "LADDER-EXT-24AL",  "name": "Extension Ladder 24ft Aluminum",        "category": "Access",          "unitCost":  20.00}},
        {"data": {"sku": "GEN-7000W-QS",     "name": "Portable Generator 7000W Quiet Series", "category": "Power",           "unitCost": 135.00}},
        {"data": {"sku": "LIGHT-JOBSITE-HV", "name": "High-Visibility Job Site Lighting Kit", "category": "Lighting",        "unitCost":  85.00}},
        {"data": {"sku": "TENT-CONTAIN-MOB", "name": "Mobile Paint Containment Tent",         "category": "Paint Equipment", "unitCost": 700.00}},
    ],
)
print(response)
package main

import (
    "context"
    "fmt"

    bem "github.com/bem-team/bem-go-sdk"
)

func main() {
    client := bem.NewClient()

    response, err := client.Collections.Items.Add(context.TODO(), bem.CollectionItemAddParams{
        CollectionName: "product_catalog",
        Items: []bem.CollectionItemAddParamsItem{
            {Data: map[string]any{"sku": "FENCE-6X12-TMP", "name": "Temporary Fence Panel 6ft x 12ft", "category": "Site Safety", "unitCost": 24.00}},
            {Data: map[string]any{"sku": "SPRAY-HVLP-PRO", "name": "HVLP Pro Paint Sprayer Kit", "category": "Paint Equipment", "unitCost": 110.00}},
            {Data: map[string]any{"sku": "LADDER-EXT-24AL", "name": "Extension Ladder 24ft Aluminum", "category": "Access", "unitCost": 20.00}},
            {Data: map[string]any{"sku": "GEN-7000W-QS", "name": "Portable Generator 7000W Quiet Series", "category": "Power", "unitCost": 135.00}},
            {Data: map[string]any{"sku": "LIGHT-JOBSITE-HV", "name": "High-Visibility Job Site Lighting Kit", "category": "Lighting", "unitCost": 85.00}},
            {Data: map[string]any{"sku": "TENT-CONTAIN-MOB", "name": "Mobile Paint Containment Tent", "category": "Paint Equipment", "unitCost": 700.00}},
        },
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", response)
}
using System.Text.Json;
using Bem;
using Bem.Models.Collections.Items;

BemClient client = new();

var response = await client.Collections.Items.Add(new ItemAddParams
{
    CollectionName = "product_catalog",
    Items = new List<ItemAddParamsItem>
    {
        new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
            {"sku": "FENCE-6X12-TMP", "name": "Temporary Fence Panel 6ft x 12ft", "category": "Site Safety", "unitCost": 24.00}
            """))),
        new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
            {"sku": "SPRAY-HVLP-PRO", "name": "HVLP Pro Paint Sprayer Kit", "category": "Paint Equipment", "unitCost": 110.00}
            """))),
        new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
            {"sku": "LADDER-EXT-24AL", "name": "Extension Ladder 24ft Aluminum", "category": "Access", "unitCost": 20.00}
            """))),
        new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
            {"sku": "GEN-7000W-QS", "name": "Portable Generator 7000W Quiet Series", "category": "Power", "unitCost": 135.00}
            """))),
        new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
            {"sku": "LIGHT-JOBSITE-HV", "name": "High-Visibility Job Site Lighting Kit", "category": "Lighting", "unitCost": 85.00}
            """))),
        new(new ItemAddParamsItemData(JsonSerializer.Deserialize<JsonElement>("""
            {"sku": "TENT-CONTAIN-MOB", "name": "Mobile Paint Containment Tent", "category": "Paint Equipment", "unitCost": 700.00}
            """))),
    },
});

Console.WriteLine(response);
bem collections:items add \
  --collection-name product_catalog \
  --item '{data: {sku: FENCE-6X12-TMP,   name: "Temporary Fence Panel 6ft x 12ft",      category: "Site Safety",     unitCost:  24.00}}' \
  --item '{data: {sku: SPRAY-HVLP-PRO,   name: "HVLP Pro Paint Sprayer Kit",            category: "Paint Equipment", unitCost: 110.00}}' \
  --item '{data: {sku: LADDER-EXT-24AL,  name: "Extension Ladder 24ft Aluminum",        category: Access,            unitCost:  20.00}}' \
  --item '{data: {sku: GEN-7000W-QS,     name: "Portable Generator 7000W Quiet Series", category: Power,             unitCost: 135.00}}' \
  --item '{data: {sku: LIGHT-JOBSITE-HV, name: "High-Visibility Job Site Lighting Kit", category: Lighting,          unitCost:  85.00}}' \
  --item '{data: {sku: TENT-CONTAIN-MOB, name: "Mobile Paint Containment Tent",         category: "Paint Equipment", unitCost: 700.00}}'

Response:

{
  "status": "pending",
  "message": "Collection items are being processed asynchronously",
  "eventID": "evt_2N6gH8ZKCmvb6BnFcGqhKJ98VzP"
}
{
  status: 'pending',
  message: 'Collection items are being processed asynchronously',
  eventID: 'evt_2N6gH8ZKCmvb6BnFcGqhKJ98VzP'
}
ItemAddResponse(
    status='pending',
    message='Collection items are being processed asynchronously',
    event_id='evt_2N6gH8ZKCmvb6BnFcGqhKJ98VzP',
)
&bem.CollectionItemAddResponse{
    Status:  "pending",
    Message: "Collection items are being processed asynchronously",
    EventID: "evt_2N6gH8ZKCmvb6BnFcGqhKJ98VzP",
}
Bem.Models.Collections.Items.ItemAddResponse {
  Status  = "pending",
  Message = "Collection items are being processed asynchronously",
  EventID = "evt_2N6gH8ZKCmvb6BnFcGqhKJ98VzP"
}
{
  "status": "pending",
  "message": "Collection items are being processed asynchronously",
  "eventID": "evt_2N6gH8ZKCmvb6BnFcGqhKJ98VzP"
}

Items are embedded asynchronously. For a catalog this small it finishes within a few seconds — call client.collections.items.retrieve({ collectionName: "product_catalog" }) (or the equivalent in your SDK) if you want to confirm before continuing.

Step 6c: Create an enrich function

The enrich function pulls every line item description out of the upstream payload, runs a semantic search against product_catalog, and writes the top match back inline to each line item at lineItems[*].matchedProduct. When the source uses [*], the target must use the same array notation so each match lands on its own line item rather than in a parallel top-level array.

curl -X POST https://api.bem.ai/v3/functions \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BEM_API_KEY" \
  -d '{
    "functionName": "sku-matcher",
    "type": "enrich",
    "displayName": "SKU Matcher",
    "tags": ["products", "sku-lookup"],
    "config": {
      "steps": [
        {
          "sourceField": "lineItems[*].description",
          "collectionName": "product_catalog",
          "targetField": "lineItems[*].matchedProduct",
          "topK": 1,
          "searchMode": "semantic"
        }
      ]
    }
  }'
import Bem from "bem-ai-sdk";

const client = new Bem();

const { function: fn } = await client.functions.create({
  functionName: "sku-matcher",
  type: "enrich",
  displayName: "SKU Matcher",
  tags: ["products", "sku-lookup"],
  config: {
    steps: [
      {
        sourceField: "lineItems[*].description",
        collectionName: "product_catalog",
        targetField: "lineItems[*].matchedProduct",
        topK: 1,
        searchMode: "semantic",
      },
    ],
  },
});

console.log(fn);
from bem import Bem

client = Bem()

response = client.functions.create(
    function_name="sku-matcher",
    type="enrich",
    display_name="SKU Matcher",
    tags=["products", "sku-lookup"],
    config={
        "steps": [
            {
                "sourceField": "lineItems[*].description",
                "collectionName": "product_catalog",
                "targetField": "lineItems[*].matchedProduct",
                "topK": 1,
                "searchMode": "semantic",
            }
        ],
    },
)
print(response.function)
package main

import (
    "context"
    "fmt"

    bem "github.com/bem-team/bem-go-sdk"
)

func main() {
    client := bem.NewClient()

    resp, err := client.Functions.New(context.TODO(), bem.FunctionNewParams{
        CreateFunction: bem.CreateFunctionUnionParam{
            OfEnrich: &bem.CreateFunctionEnrichParam{
                FunctionName: "sku-matcher",
                DisplayName:  bem.String("SKU Matcher"),
                Tags:         []string{"products", "sku-lookup"},
                Config: bem.EnrichConfigParam{
                    Steps: []bem.EnrichStepParam{
                        {
                            SourceField:    "lineItems[*].description",
                            CollectionName: "product_catalog",
                            TargetField:    "lineItems[*].matchedProduct",
                            TopK:           bem.Int(1),
                            SearchMode:     bem.EnrichStepSearchModeSemantic,
                        },
                    },
                },
            },
        },
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", resp.Function)
}
using Bem;
using Bem.Models.Functions;

BemClient client = new();

var response = await client.Functions.Create(new FunctionCreateParams
{
    CreateFunction = new Enrich
    {
        FunctionName = "sku-matcher",
        DisplayName  = "SKU Matcher",
        Tags         = new List<string> { "products", "sku-lookup" },
        Config = new EnrichConfig
        {
            Steps = new List<EnrichStep>
            {
                new EnrichStep
                {
                    SourceField    = "lineItems[*].description",
                    CollectionName = "product_catalog",
                    TargetField    = "lineItems[*].matchedProduct",
                    TopK           = 1,
                    SearchMode     = EnrichStepSearchMode.Semantic,
                },
            },
        },
    },
});

Console.WriteLine(response.Function);
bem functions create \
  --function-name sku-matcher \
  --type enrich \
  --display-name "SKU Matcher" \
  --tag products --tag sku-lookup \
  --config '{steps: [{sourceField: "lineItems[*].description", collectionName: product_catalog, targetField: "lineItems[*].matchedProduct", topK: 1, searchMode: semantic}]}'

Response:

{
  "function": {
    "functionID": "fn_7ghi789jkl",
    "functionName": "sku-matcher",
    "displayName": "SKU Matcher",
    "type": "enrich",
    "currentVersionNum": 1
  }
}
{
  functionID: 'fn_7ghi789jkl',
  functionName: 'sku-matcher',
  displayName: 'SKU Matcher',
  type: 'enrich',
  currentVersionNum: 1
}
Function(
    function_id='fn_7ghi789jkl',
    function_name='sku-matcher',
    display_name='SKU Matcher',
    type='enrich',
    current_version_num=1,
)
bem.Function{
    FunctionID:        "fn_7ghi789jkl",
    FunctionName:      "sku-matcher",
    DisplayName:       "SKU Matcher",
    Type:              "enrich",
    CurrentVersionNum: 1,
}
Bem.Models.Functions.Function {
  FunctionID        = "fn_7ghi789jkl",
  FunctionName      = "sku-matcher",
  DisplayName       = "SKU Matcher",
  Type              = "enrich",
  CurrentVersionNum = 1
}
{
  "function": {
    "functionID": "fn_7ghi789jkl",
    "functionName": "sku-matcher",
    "displayName": "SKU Matcher",
    "type": "enrich",
    "currentVersionNum": 1
  }
}

Step 6d: Chain the enrich function into the workflow

PATCH /v3/workflows/{workflowName} takes the full desired node/edge topology and creates a new workflow version. Add sku-matcher as a second node, then connect invoice-extractor → sku-matcher with an edge.

curl -X PATCH https://api.bem.ai/v3/workflows/invoice-processing \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BEM_API_KEY" \
  -d '{
    "mainNodeName": "invoice-extractor",
    "nodes": [
      { "name": "invoice-extractor", "function": { "name": "invoice-extractor" } },
      { "name": "sku-matcher",       "function": { "name": "sku-matcher" } }
    ],
    "edges": [
      { "sourceNodeName": "invoice-extractor", "destinationNodeName": "sku-matcher" }
    ]
  }'
import Bem from "bem-ai-sdk";

const client = new Bem();

const { workflow } = await client.workflows.update("invoice-processing", {
  mainNodeName: "invoice-extractor",
  nodes: [
    { name: "invoice-extractor", function: { name: "invoice-extractor" } },
    { name: "sku-matcher",       function: { name: "sku-matcher" } },
  ],
  edges: [
    { sourceNodeName: "invoice-extractor", destinationNodeName: "sku-matcher" },
  ],
});

console.log(workflow);
from bem import Bem

client = Bem()

response = client.workflows.update(
    "invoice-processing",
    main_node_name="invoice-extractor",
    nodes=[
        {"name": "invoice-extractor", "function": {"name": "invoice-extractor"}},
        {"name": "sku-matcher",       "function": {"name": "sku-matcher"}},
    ],
    edges=[
        {"source_node_name": "invoice-extractor", "destination_node_name": "sku-matcher"},
    ],
)
print(response.workflow)
package main

import (
    "context"
    "fmt"

    bem "github.com/bem-team/bem-go-sdk"
)

func main() {
    client := bem.NewClient()

    resp, err := client.Workflows.Update(context.TODO(), "invoice-processing", bem.WorkflowUpdateParams{
        MainNodeName: bem.String("invoice-extractor"),
        Nodes: []bem.WorkflowUpdateParamsNode{
            {
                Name:     bem.String("invoice-extractor"),
                Function: bem.FunctionVersionIdentifierParam{Name: bem.String("invoice-extractor")},
            },
            {
                Name:     bem.String("sku-matcher"),
                Function: bem.FunctionVersionIdentifierParam{Name: bem.String("sku-matcher")},
            },
        },
        Edges: []bem.WorkflowUpdateParamsEdge{
            {
                SourceNodeName:      "invoice-extractor",
                DestinationNodeName: "sku-matcher",
            },
        },
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", resp.Workflow)
}
using Bem;
using Bem.Models.Workflows;

BemClient client = new();

var response = await client.Workflows.Update("invoice-processing", new WorkflowUpdateParams
{
    MainNodeName = "invoice-extractor",
    Nodes = new List<WorkflowUpdateParamsNode>
    {
        new WorkflowUpdateParamsNode { Function = new FunctionVersionIdentifier { Name = "invoice-extractor" }, Name = "invoice-extractor" },
        new WorkflowUpdateParamsNode { Function = new FunctionVersionIdentifier { Name = "sku-matcher" },       Name = "sku-matcher" },
    },
    Edges = new List<WorkflowUpdateParamsEdge>
    {
        new WorkflowUpdateParamsEdge { SourceNodeName = "invoice-extractor", DestinationNodeName = "sku-matcher" },
    },
});

Console.WriteLine(response.Workflow);
bem workflows update \
  --workflow-name invoice-processing \
  --main-node-name invoice-extractor \
  --node '{name: invoice-extractor, function: {name: invoice-extractor}}' \
  --node '{name: sku-matcher, function: {name: sku-matcher}}' \
  --edge '{sourceNodeName: invoice-extractor, destinationNodeName: sku-matcher}'

Response:

{
  "workflow": {
    "id": "wf_2def456abc",
    "name": "invoice-processing",
    "displayName": "Invoice Processing Workflow",
    "versionNum": 2,
    "mainNodeName": "invoice-extractor",
    "nodes": [
      { "name": "invoice-extractor", "function": { "name": "invoice-extractor", "versionNum": 1 } },
      { "name": "sku-matcher",       "function": { "name": "sku-matcher",       "versionNum": 1 } }
    ],
    "edges": [
      { "sourceNodeName": "invoice-extractor", "destinationNodeName": "sku-matcher" }
    ]
  }
}
{
  id: 'wf_2def456abc',
  name: 'invoice-processing',
  displayName: 'Invoice Processing Workflow',
  versionNum: 2,
  mainNodeName: 'invoice-extractor',
  nodes: [
    { name: 'invoice-extractor', function: { name: 'invoice-extractor', versionNum: 1 } },
    { name: 'sku-matcher',       function: { name: 'sku-matcher',       versionNum: 1 } }
  ],
  edges: [
    { sourceNodeName: 'invoice-extractor', destinationNodeName: 'sku-matcher' }
  ]
}
Workflow(
    id='wf_2def456abc',
    name='invoice-processing',
    display_name='Invoice Processing Workflow',
    version_num=2,
    main_node_name='invoice-extractor',
    nodes=[
        WorkflowNodeResponse(name='invoice-extractor', function=FunctionVersionIdentifier(name='invoice-extractor', version_num=1)),
        WorkflowNodeResponse(name='sku-matcher',       function=FunctionVersionIdentifier(name='sku-matcher',       version_num=1)),
    ],
    edges=[
        WorkflowEdgeResponse(source_node_name='invoice-extractor', destination_node_name='sku-matcher'),
    ],
)
bem.Workflow{
    ID:           "wf_2def456abc",
    Name:         "invoice-processing",
    DisplayName:  "Invoice Processing Workflow",
    VersionNum:   2,
    MainNodeName: "invoice-extractor",
    Nodes: []bem.WorkflowNodeResponse{
        {Name: "invoice-extractor", Function: bem.FunctionVersionIdentifier{Name: "invoice-extractor", VersionNum: 1}},
        {Name: "sku-matcher",       Function: bem.FunctionVersionIdentifier{Name: "sku-matcher",       VersionNum: 1}},
    },
    Edges: []bem.WorkflowEdgeResponse{
        {SourceNodeName: "invoice-extractor", DestinationNodeName: "sku-matcher"},
    },
}
Bem.Models.Workflows.Workflow {
  ID           = "wf_2def456abc",
  Name         = "invoice-processing",
  DisplayName  = "Invoice Processing Workflow",
  VersionNum   = 2,
  MainNodeName = "invoice-extractor",
  Nodes        = [
    WorkflowNodeResponse { Name = "invoice-extractor", Function = FunctionVersionIdentifier { Name = "invoice-extractor", VersionNum = 1 } },
    WorkflowNodeResponse { Name = "sku-matcher",       Function = FunctionVersionIdentifier { Name = "sku-matcher",       VersionNum = 1 } }
  ],
  Edges        = [
    WorkflowEdgeResponse { SourceNodeName = "invoice-extractor", DestinationNodeName = "sku-matcher" }
  ]
}
{
  "workflow": {
    "id": "wf_2def456abc",
    "name": "invoice-processing",
    "displayName": "Invoice Processing Workflow",
    "versionNum": 2,
    "mainNodeName": "invoice-extractor",
    "nodes": [
      { "name": "invoice-extractor", "function": { "name": "invoice-extractor", "versionNum": 1 } },
      { "name": "sku-matcher",       "function": { "name": "sku-matcher",       "versionNum": 1 } }
    ],
    "edges": [
      { "sourceNodeName": "invoice-extractor", "destinationNodeName": "sku-matcher" }
    ]
  }
}

Call the enriched workflow

Invoke the workflow the same way as Step 5 — no client changes required. Because sku-matcher is now the terminal node, the call's outputs array contains an enrich event whose enrichedContent carries the extracted invoice with each line item's best catalog match attached inline at lineItems[i].matchedProduct.

All six line items on the sample invoice come back with a matchedProduct; the responses below show the first two. cosineDistance is the semantic distance to the match — lower is closer — so you can threshold on it to reject weak matches.

{
  "call": {
    "callID": "wc_8mno012pqr",
    "status": "completed",
    "workflowName": "invoice-processing",
    "workflowVersionNum": 2,
    "outputs": [
      {
        "eventID": "evt_5abc678def",
        "eventType": "enrich",
        "functionName": "sku-matcher",
        "enrichedContent": {
          "invoiceNumber": "FE-2421 C",
          "lineItems": [
            {
              "description": "Temporary Fence Panel, 6ft x 12ft",
              "quantity": 40,
              "unitPrice": 22.0,
              "rateUnit": "panel",
              "amount": 880.0,
              "matchedProduct": {
                "data": { "sku": "FENCE-6X12-TMP", "name": "Temporary Fence Panel 6ft x 12ft", "category": "Site Safety", "unitCost": 24.00 },
                "cosineDistance": 0.0412
              }
            },
            {
              "description": "Paint Sprayer Kit, HVLP Pro",
              "quantity": 3,
              "unitPrice": 95.0,
              "rateUnit": "day",
              "amount": 285.0,
              "matchedProduct": {
                "data": { "sku": "SPRAY-HVLP-PRO", "name": "HVLP Pro Paint Sprayer Kit", "category": "Paint Equipment", "unitCost": 110.00 },
                "cosineDistance": 0.0687
              }
            }
          ]
        }
      }
    ],
    "errors": [],
    "url": "/v3/calls/wc_8mno012pqr",
    "traceUrl": "/v3/calls/wc_8mno012pqr/trace"
  }
}
{
  callID: 'wc_8mno012pqr',
  status: 'completed',
  workflowName: 'invoice-processing',
  workflowVersionNum: 2,
  outputs: [
    {
      eventID: 'evt_5abc678def',
      eventType: 'enrich',
      functionName: 'sku-matcher',
      enrichedContent: {
        invoiceNumber: 'FE-2421 C',
        lineItems: [
          {
            description: 'Temporary Fence Panel, 6ft x 12ft',
            quantity: 40,
            unitPrice: 22,
            rateUnit: 'panel',
            amount: 880,
            matchedProduct: { data: { sku: 'FENCE-6X12-TMP', name: 'Temporary Fence Panel 6ft x 12ft', category: 'Site Safety', unitCost: 24 }, cosineDistance: 0.0412 }
          },
          {
            description: 'Paint Sprayer Kit, HVLP Pro',
            quantity: 3,
            unitPrice: 95,
            rateUnit: 'day',
            amount: 285,
            matchedProduct: { data: { sku: 'SPRAY-HVLP-PRO', name: 'HVLP Pro Paint Sprayer Kit', category: 'Paint Equipment', unitCost: 110 }, cosineDistance: 0.0687 }
          }
        ]
      }
    }
  ],
  errors: [],
  url: '/v3/calls/wc_8mno012pqr',
  traceUrl: '/v3/calls/wc_8mno012pqr/trace'
}
CallV3(
    call_id='wc_8mno012pqr',
    status='completed',
    workflow_name='invoice-processing',
    workflow_version_num=2,
    outputs=[
        EnrichEvent(
            event_id='evt_5abc678def',
            event_type='enrich',
            function_name='sku-matcher',
            enriched_content={
                'invoiceNumber': 'FE-2421 C',
                'lineItems': [
                    {
                        'description': 'Temporary Fence Panel, 6ft x 12ft',
                        'quantity': 10,
                        'unitPrice': 25.0,
                        'amount': 250.0,
                        'matchedProduct': {'data': {'sku': 'FENCE-6X12-TMP', 'name': 'Temporary Fence Panel 6ft x 12ft', 'category': 'Site Safety', 'unitCost': 24.00}, 'cosineDistance': 0.0412},
                    },
                    {
                        'description': 'Paint Sprayer Kit, HVLP Pro',
                        'quantity': 5,
                        'unitPrice': 15.0,
                        'amount': 75.0,
                        'matchedProduct': {'data': {'sku': 'SPRAY-HVLP-PRO', 'name': 'HVLP Pro Paint Sprayer Kit', 'category': 'Paint Equipment', 'unitCost': 110.00}, 'cosineDistance': 0.0687},
                    },
                ],
            },
        )
    ],
    errors=[],
    url='/v3/calls/wc_8mno012pqr',
    trace_url='/v3/calls/wc_8mno012pqr/trace',
)
bem.CallV3{
    CallID:             "wc_8mno012pqr",
    Status:             "completed",
    WorkflowName:       "invoice-processing",
    WorkflowVersionNum: 2,
    Outputs: []bem.Event{
        {
            EventID:      "evt_5abc678def",
            EventType:    "enrich",
            FunctionName: "sku-matcher",
            EnrichedContent: map[string]any{
                "invoiceNumber": "FE-2421 C",
                "lineItems": []any{
                    map[string]any{
                        "description": "Temporary Fence Panel, 6ft x 12ft",
                        "quantity":    10,
                        "unitPrice":   25.0,
                        "amount":      250.0,
                        "matchedProduct": map[string]any{
                            "data":           map[string]any{"sku": "FENCE-6X12-TMP", "name": "Temporary Fence Panel 6ft x 12ft", "category": "Site Safety", "unitCost": 24.00},
                            "cosineDistance": 0.0823,
                        },
                    },
                    map[string]any{
                        "description": "Paint Sprayer Kit, HVLP Pro",
                        "quantity":    5,
                        "unitPrice":   15.0,
                        "amount":      75.0,
                        "matchedProduct": map[string]any{
                            "data":           map[string]any{"sku": "SPRAY-HVLP-PRO", "name": "HVLP Pro Paint Sprayer Kit", "category": "Paint Equipment", "unitCost": 110.00},
                            "cosineDistance": 0.1104,
                        },
                    },
                },
            },
        },
    },
    Errors:   []bem.ErrorEvent{},
    URL:      "/v3/calls/wc_8mno012pqr",
    TraceURL: "/v3/calls/wc_8mno012pqr/trace",
}
Bem.Models.Calls.CallV3 {
  CallID             = "wc_8mno012pqr",
  Status             = "completed",
  WorkflowName       = "invoice-processing",
  WorkflowVersionNum = 2,
  Outputs = [
    EnrichEvent {
      EventID      = "evt_5abc678def",
      EventType    = "enrich",
      FunctionName = "sku-matcher",
      EnrichedContent = {
        "invoiceNumber": "FE-2421 C",
        "lineItems": [
          {
            "description":    "Temporary Fence Panel, 6ft x 12ft",
            "quantity":       10,
            "unitPrice":      25.0,
            "amount":         250.0,
            "matchedProduct": { "data": { "sku": "FENCE-6X12-TMP", ... }, "cosineDistance": 0.0412 }
          },
          {
            "description":    "Paint Sprayer Kit, HVLP Pro",
            "quantity":       5,
            "unitPrice":      15.0,
            "amount":         75.0,
            "matchedProduct": { "data": { "sku": "SPRAY-HVLP-PRO", ... }, "cosineDistance": 0.0687 }
          }
        ]
      }
    }
  ],
  Errors   = [],
  Url      = "/v3/calls/wc_8mno012pqr",
  TraceUrl = "/v3/calls/wc_8mno012pqr/trace"
}
{
  "call": {
    "callID": "wc_8mno012pqr",
    "status": "completed",
    "workflowName": "invoice-processing",
    "workflowVersionNum": 2,
    "outputs": [
      {
        "eventID": "evt_5abc678def",
        "eventType": "enrich",
        "functionName": "sku-matcher",
        "enrichedContent": {
          "invoiceNumber": "FE-2421 C",
          "lineItems": [
            {
              "description": "Temporary Fence Panel, 6ft x 12ft",
              "quantity": 40,
              "unitPrice": 22.0,
              "rateUnit": "panel",
              "amount": 880.0,
              "matchedProduct": {
                "data": { "sku": "FENCE-6X12-TMP", "name": "Temporary Fence Panel 6ft x 12ft", "category": "Site Safety", "unitCost": 24.00 },
                "cosineDistance": 0.0412
              }
            },
            {
              "description": "Paint Sprayer Kit, HVLP Pro",
              "quantity": 3,
              "unitPrice": 95.0,
              "rateUnit": "day",
              "amount": 285.0,
              "matchedProduct": {
                "data": { "sku": "SPRAY-HVLP-PRO", "name": "HVLP Pro Paint Sprayer Kit", "category": "Paint Equipment", "unitCost": 110.00 },
                "cosineDistance": 0.0687
              }
            }
          ]
        }
      }
    ],
    "errors": [],
    "url": "/v3/calls/wc_8mno012pqr",
    "traceUrl": "/v3/calls/wc_8mno012pqr/trace"
  }
}

The intermediate extract event is still available via GET /v3/calls/{callID}/trace if you need to inspect each function call in the DAG.

Webhook delivery (optional)

For production pipelines that may exceed the 30-second wait window, subscribe a webhook so bem pushes results to your server the moment processing completes.

curl -X POST https://api.bem.ai/v1-alpha/subscriptions \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BEM_API_KEY" \
  -d '{
    "name": "invoice-results",
    "type": "transform",
    "functionName": "invoice-extractor",
    "webhookURL": "https://your-server.com/webhooks/bem"
  }'

Verify incoming requests with the bem-signature header — see Webhook Authentication for the HMAC-SHA256 recipe.

Next Steps

On this page