LLM-readable documentation index

Reading Workflow Call Outputs

Where the extracted data lives in a POST /v3/workflows/{name}/call response — the path, the per-event-type field map, and accessor patterns in every SDK

Hand off to an LLM

When you call a workflow with wait=true (or fetch a completed call via GET /v3/calls/{callID}), the response is the same shape — a call object whose terminal events are in call.outputs. The extracted data lives on each event, not under a nested transformation field.

result (or response object)
  └─ call
       ├─ callID
       ├─ status                ("completed" | "pending" | "running" | "failed")
       ├─ outputs[]             array of terminal events
       │    ├─ eventID
       │    ├─ eventType        discriminator → tells you which payload field to read
       │    ├─ functionName
       │    └─ <payload>        transformedContent | enrichedContent | choice | …
       ├─ errors[]
       ├─ url                   /v3/calls/{callID}
       └─ traceUrl              /v3/calls/{callID}/trace  (full per-node execution graph)

callType tells you what kind of call you're looking at

GET /v3/calls and GET /v3/calls/{callID} don't only return workflow calls — they also surface direct_function and adhoc_function calls made outside V3. Every call object carries callType (workflow | direct_function | adhoc_function); workflow calls populate workflowID / workflowName / workflowVersionNum, and function calls populate functionID / functionName / functionType / functionVersionNum instead. This page covers the workflow-call shape — outputs[] and errors[] work the same regardless of callType.

GET /v3/calls filters on callType with callTypes, and on the function-scoped fields with functionIDs / functionNames. To narrow a call log down to failed ad-hoc function calls for one function:

curl -G "https://api.bem.ai/v3/calls" \
  -H "x-api-key: $BEM_API_KEY" \
  -d callTypes=adhoc_function \
  -d statuses=failed \
  -d functionNames=invoice-extractor

GET /v3/calls takes several more filters — see List Calls for the complete parameter list.

outputs is always an array

The terminal nodes of a workflow are whatever has no outgoing edge. A single-step workflow has exactly one terminal event; a branching workflow (Classify) or a fan-out workflow (Split) can have more. Even with one terminal node, the field is plural — you index into it.

result.call.outputs[0].transformedContent; // ✓
result.call.output; // ✗ does not exist
result.call.outputs.transformedContent; // ✗ outputs is an array

Where the data lives, by eventType

The payload field name depends on the event variant. Switch on eventType (or use the SDK's variant accessor) to get the right field.

eventTypeContent fieldProduced by
extracttransformedContentExtract function
enrichenrichedContent (carries upstream content + matched data)Enrich function
jointransformedContentJoin function
classifychoice (the picked classification label) — the extracted JSON flows from the routed-to extractor downstreamClassify function
payload_shapingtransformedContent (the reshaped JSON from the JMESPath schema)Payload Shaping function
transform, analyzetransformedContentLegacy V1/V2 functions (still readable from V3)
routeroute metadataLegacy V1/V2 function
senddelivery status (no content payload)Send function
parsetransformedContent (sections/entities/relationships JSON, same field as extract)Parse function
errorerrorMessage, errorCode, errorDetailsAny failed function call

So an Extract-only workflow lands at outputs[0].transformedContent. An Extract → Enrich workflow lands at outputs[0].enrichedContent (because Enrich is the terminal node). A Classify-only workflow lands at outputs[0].choice.

A Parse function fills outputs[0].transformedContent with its sections/entities/relationships JSON like any other terminal function. POST /v3/fs is an additional navigation layer for querying parsed corpora, not a substitute for reading the parse result off the call output.

Accessor patterns

The same path in every language. The SDKs flatten the event variants into a single discriminated union — you can either read the field directly (it'll be null/missing on the wrong variant) or use the SDK's variant accessor for type safety.

# Extract-only workflow
jq '.call.outputs[0].transformedContent' < response.json

# Extract → Enrich workflow
jq '.call.outputs[0].enrichedContent' < response.json

# Switch on eventType
jq '.call.outputs[] | if .eventType=="enrich" then .enrichedContent else .transformedContent end' < response.json
const { call } = await client.workflows.call("my-workflow", { wait: true, /* ... */ });

// Single terminal node (Extract)
const data = call?.outputs?.[0]?.transformedContent;

// Single terminal node (Enrich)
const enriched = call?.outputs?.[0]?.enrichedContent;

// Generic — switch on eventType
for (const event of call?.outputs ?? []) {
  switch (event.eventType) {
    case "extract":
    case "transform":
    case "join":
      console.log(event.transformedContent);
      break;
    case "enrich":
      console.log(event.enrichedContent);
      break;
    case "classify":
      console.log(event.choice);
      break;
  }
}
response = client.workflows.call("my-workflow", wait=True, ...)
call = response.call

# Single terminal node (Extract)
data = call.outputs[0].transformed_content

# Single terminal node (Enrich)
enriched = call.outputs[0].enriched_content

# Generic — switch on event_type
for event in call.outputs:
    if event.event_type in ("extract", "transform", "join"):
        print(event.transformed_content)
    elif event.event_type == "enrich":
        print(event.enriched_content)
    elif event.event_type == "classify":
        print(event.choice)
resp, err := client.Workflows.Call(ctx, "my-workflow", bem.WorkflowCallParams{Wait: bem.Bool(true), /* ... */})
if err != nil { panic(err) }

// Direct field access (works because the union flattens all variant fields)
data := resp.Call.Outputs[0].TransformedContent  // for extract / transform / join
enriched := resp.Call.Outputs[0].EnrichedContent // for enrich

// Type-safe variant switch
for _, event := range resp.Call.Outputs {
    switch v := event.AsAny().(type) {
    case bem.EventExtract:
        fmt.Println(v.TransformedContent)
    case bem.EventEnrich:
        fmt.Println(v.EnrichedContent)
    case bem.EventClassify:
        fmt.Println(v.Choice)
    }
}
var response = await client.Workflows.Call("my-workflow", new WorkflowCallParams { Wait = true, /* ... */ });
var call = response.Call;

// Single terminal node (Extract)
var data = call.Outputs[0].TransformedContent;

// Single terminal node (Enrich)
var enriched = call.Outputs[0].EnrichedContent;

// Generic — pattern match on event variant
foreach (var ev in call.Outputs)
{
    switch (ev)
    {
        case ExtractEvent ex:   Console.WriteLine(ex.TransformedContent); break;
        case EnrichEvent en:    Console.WriteLine(en.EnrichedContent);    break;
        case ClassifyEvent cl:  Console.WriteLine(cl.Choice);             break;
    }
}
bem workflows call --workflow-name my-workflow --wait --call-reference-id my-ref \
  --input.single-file '{"inputContent": "@my-file.pdf", "inputType": "pdf"}' \
  --transform 'call.outputs.0.transformedContent'

The --transform flag uses GJSON syntax and projects to the path you ask for, so the CLI prints just the extracted JSON. Replace transformedContent with enrichedContent for an Enrich-terminal workflow.

_metadata echoes back what you attached to the call

Attach an arbitrary JSON object to a call and bem writes it back into the extracted output, so you can correlate a result with your own systems — a customer ID, a batch ID, a trace span — without a second lookup.

Attaching it

Pass metadata alongside input on POST /v3/workflows/{workflowName}/call (or a direct call create). Both the JSON and multipart request bodies accept it, but the shape differs:

  • JSON bodymetadata is a normal JSON object.
  • multipart/form-datametadata is that same object serialized to a string, because multipart form fields are always strings.

Both forms are validated identically: metadata must be a JSON object — not an array, string, number, or boolean — and at most 4 KB serialized. Either violation returns 400 Bad Request with invalid metadata: <reason>.

# JSON body
curl -X POST "https://api.bem.ai/v3/workflows/invoice-processing/call" \
  -H "x-api-key: $BEM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callReferenceID": "invoice-001",
    "metadata": { "customerID": "cust-42", "region": "us-east-1" },
    "input": { "singleFile": { "inputContent": "@invoice.pdf", "inputType": "pdf" } }
  }'

# multipart/form-data — metadata is the same object, JSON-encoded into a string field
curl -X POST "https://api.bem.ai/v3/workflows/invoice-processing/call" \
  -H "x-api-key: $BEM_API_KEY" \
  -F 'callReferenceID=invoice-001' \
  -F 'metadata={"customerID":"cust-42","region":"us-east-1"}' \
  -F 'file=@invoice.pdf'

Where it comes back

bem writes your metadata into a reserved _metadata key inside transformedContent, next to a referenceID field that mirrors the callReferenceID you passed on the call (see Idempotency via callReferenceID).

Without metadata on the call:

{
  "invoiceNumber": "FE-2421 C",
  "vendor": { "name": "Appleseed Paint Co." },
  "totalAmount": 2678.51
}

With metadata: { customerID: 'cust-42', region: 'us-east-1' } on the call:

{
  "invoiceNumber": "FE-2421 C",
  "vendor": { "name": "Appleseed Paint Co." },
  "totalAmount": 2678.51,
  "_metadata": {
    "customerID": "cust-42",
    "region": "us-east-1",
    "referenceID": "invoice-001"
  }
}

_metadata only appears when the call carried at least one non-reserved metadata key. No metadata on the call — or metadata made up entirely of reserved keys — means no _metadata key at all, not an empty object. Guard the read: output.transformedContent._metadata?.customerID, not output.transformedContent._metadata.customerID.

Which event types echo it

Only event types whose payload comes from a transformation carry _metadata. The rest have no ExtractedJSON for the platform to inject into.

Echoes _metadataDoesn't
extract, transform, parse, analyze, joinenrich, classify, payload_shaping, route, send, error

Reserved keys aren't echoed

Metadata keys starting with _ are reserved for the platform and are stripped before _metadata is built — they never reach the response, however you send them. The same _-prefix convention shows up elsewhere, e.g. Render's reserved underscore prefix for template placeholders. Don't rely on an underscore-prefixed key of your own surviving the round trip.

Anti-patterns

  • call.output (singular) — does not exist. The field is always outputs[].
  • outputs[0].transformation.extractedJSON — that's the legacy V1/V2 Transformation record shape returned by GET /v1-beta/transformations. V3 workflow calls return events whose payload sits at the top of each event object as transformedContent / enrichedContent / choice / etc.
  • Skipping the outputs indexoutputs is an array even when there's a single terminal event.
  • Reading transformedContent on an enrich event — empty or missing. Enrich's payload is at enrichedContent. Switch on eventType.
  • outputs[0].transformedContent._metadata.customerID unguarded_metadata is only present when the call carried non-reserved metadata. See _metadata echoes back what you attached to the call.

For per-node execution detail (every intermediate function call, not just the terminal nodes), fetch GET /v3/calls/{callID}/trace.

On this page