Skip to main content
Updated Nov 1, 2026

CEL expression editor

Use CEL expressions for agent messages, action conditions, workflow outputs and form behavior. AutoTalk uses CEL-Go in both browser and server. Shared platform calls use <family>.<camelCase>; imported tenant calls use <slug>.<function>. The function reference shows the syntax and availability for each context.

Generation 4 keeps the same {expr, ...options} descriptor and uses shorter function names. CEL API writes send X-Autotalk-Cel-Schema: 4; MCP tools that accept CEL source use celSchemaGeneration: "4". Old prefixed source needs the reviewed migration before it can run with this generation.

Where expressions appear​

The CEL badge identifies an expression field. Common locations include agent messages and pre/post-actions, workflow steps, templates, and Custom Type labels/visibility/previews. Field names or literal tab/reference identifiers are not automatically expressions.

Compact editor​

The compact editor provides syntax highlighting, validation, formatting and an Expand button. Use Ctrl+Shift+F to format. Invalid syntax/type diagnostics help locate the problem; tests also check behavior with actual variable values.

Call names are highlighted from syntax, including unknown calls. Namespace coloring uses the current callable/import metadata; a data selection such as values.name remains data. Color alone does not confirm that a function is available or valid.

Compact native CEL field with a pinned tenant library

The equivalent generation-4 call is labels.normalizeName from the pinned v1 release.

The screenshots below use synthetic demo data from generation 3; their autotalk.*/tenant.* labels predate the short names shown in these examples.

Native CEL and literal text​

Each expression field stores one native CEL expression. For plain text, write a quoted CEL string and escape any quotes or newlines inside it. The expanded editor shows that same expression. For dynamic text, write "Hello " + contact.name + "!", or use an explicit template call:

text.template("Hello {name}!", {
"name": values.coalesce(maps.get(contact, "name"), "Guest")
})

Braces such as {{contact.name}} inside a quoted literal stay literal text.

Quoted CEL string containing literal double braces

"Hello {{contact.name}}" is literal text; the braces do not substitute a value.

JSON and MCP use the same descriptor everywhere, including Custom Type labels, tabs, previews and monitor conditions:

{"expr": "\"Hello \" + contact.name + \"!\"", "onFailure": "throw"}

Advanced field options​

OptionPurpose
On failureThrow, return null, use a literal fallback string, or keep the authored expression
Result typeExpected application result: any, string, number, boolean, date, array or object
Fallback valueLiteral string used by the fallback policy

A fallback does not make an invalid expression correct. Test the successful result and failure cases before relying on it.

Full workbench​

Expand the field to open the workbench. Its toolbar opens Explorer and Test. The compact and expanded editors share the owning document's exact library imports, so validation, completion and Test use the same releases. Save the owning agent, workflow, Custom Type or library draft to persist the expression and its pins.

Explorer​

Explorer lists context variables and callable functions with signatures. Shared helpers are grouped by family, such as maps and text; standard CEL features retain their own syntax. Exact imports add root-slug functions, such as labels.normalizeName. Search and select entries to insert them; functions unavailable in this context are excluded.

Explorer showing a shared native function and its signature

Explorer filters the current field's available functions and shows their signatures.

ContextTypical inputs
Agentcontact, contactMessage, conversation, company, declared tool parameters
Workflow_workflow, occurrenceDate, declared manual inputs and trigger-specific doc, prev, op, model
Formvalues, item, args, company, employee when supplied by that form
Library functionOnly its declared named parameters and lexical bindings

The contact variable is contact. Manual/tool arguments are top-level names, not an implicit parameters map. Obtain employee data with a data action when needed in an agent, then read its declared step output.

Test​

  1. Supply representative JSON variable values.
  2. Run with the button or Ctrl+Enter.
  3. Compare the value/type or inspect the diagnostic.

Native CEL Test returning the normalized demo label

Test evaluates the pinned pure function and returns example label.

Include missing/null/empty data and boundary cases. Test does not automatically run workflow actions to create step outputs; context helpers and imported functions need the owning field's context and exact library bundle.

Optional values and typed logic​

Missing roots and direct missing-field access produce errors. Use explicit safe paths or optional selection for optional data:

values.coalesce(maps.get(contact, "name"), "Guest")
contact.?nickname.orValue("Guest")
cel.bind(name, maps.get(contact, "name"),
values.present(name) ? name : "Guest")

Presence and null are distinct: a present null does not use an optional fallback. Native has(contact.name) tests presence; maps.has(contact,"name") tests a non-null path. Native conversions use CEL types and can fail. Preserve external time units explicitly with datetime.fromMillis / toMillis; native timestamp(int) uses Unix seconds.

Step output examples​

workflow.stepOk(0)
workflow.stepData(0, "title", "Unknown")
workflow.stepData(1, "choices.0.message.content", "")
workflow.stepError(2)

Paths are relative to the step's data envelope when present; do not repeat data in the path. Root-shaped actions use their declared root fields. See step data and root-shaped steps.

Execute code publishes result, so workflow.stepData(N,"count") reads workflow.step(N).result.count. Root siblings files, logs and executionTimeMs need maps.get(workflow.step(N),"files",[]). A missing path returns the default, but a nonexistent step index is an error.

Tenant libraries​

Libraries let your tenant reuse business rules without repeating a large expression. Open Automations → Libraries and choose New library → CEL. Enter the name and a stable namespace (slug), then choose Create library to open the function editor. Add typed functions and fixtures, then use Verify to check compilation and tests. Save the draft with the page Save control. Publish vN publishes the saved revision and reruns compilation and configured fixtures.

Each function has named parameters, a return type and a CEL expression body. It can read only those parameters, local bindings, pure shared helpers and imported pure tenant functions. Pass contact records, clocks and other facts explicitly; a library cannot read ambient agent/form/step state or perform I/O.

Example draft​

{
"imports": [],
"functions": [{
"name": "normalizeName",
"parameters": [{"name": "value", "type": "string"}],
"returnType": "string",
"body": {"expr": "text.nameKey(value)"},
"tests": [
{"name": "accents-and-spaces", "args": {"value": " Álice Silva "}, "expected": "alice silva"},
{"name": "empty", "args": {"value": ""}, "expected": ""}
]
}]
}

Use this draft in a library whose slug is labels. Fixtures supply exactly the named arguments and exactly one of expected or expectedError. Expected errors match evaluator codes. Types support CEL primitives, list<T>, map<K,V> and optional<T>. Special JSON fixtures use the $cel value codec, for example:

{"$cel": "timestamp", "seconds": "1705276800", "nanos": 0}

Publish and select an exact release​

Published releases are immutable. For a first import, select a readable unimported tenant function from completion, or apply its missing-import quick fix; this adds the exact release to the owning document. Once imports exist, open the library count beside the document/page title to manage them and choose a different published version. An expression that calls an imported function also offers Manage from its footer library chip. The manager shares live imports with the compact and expanded editors. Save the owning document to persist changes. For MCP/API or JSON mode, use the exact returned IDs:

{"celLibraries": [{"libraryId": "000000000000000000000001", "releaseId": "000000000000000000000002"}]}

When an owning document omits celLibraries, it uses the company's default imports. Its own array replaces those defaults; [] explicitly disables them. Existing documents with an empty array keep that opt-out. Agent/workflow messages use their execution owner's resolved imports, and templates use their template's imports. Messages sent through the API, contacts.sendMessage, or the employee conversation UI save a snapshot of the exact imports when sent, so later company changes do not change option actions on existing messages. The conversation's assigned agent does not supply imports for an employee message.

Direct executable option actions require read access to their imported libraries. The curated MCP send_message tool does not accept option actions and rejects them before sending; use contacts.sendMessage with automation:admin for those messages.

Replace the placeholders with your library/release IDs. You can then call:

labels.normalizeName(
values.coalesce(maps.get(contact, "name"), ""))

Draft edits and new publications never advance existing callers automatically. Select a new exact release deliberately; dependent libraries also pin exact releases. Reload a stale draft revision before saving or publishing again.

labels.normalizeName(value) is a tenant-defined function in this example. Each tenant must define and import its own labels library; the namespace does not provide an AutoTalk built-in or another tenant's implementation.

MCP and API authoring​

MCP clients use create_document / update_document with type:"cel_libraries". Updates include the currently read draftRevision. Discover lifecycle actions with list_functions, then use execute_function for validateDraft, testDraft and publish:

For CEL-bearing library writes, pass celSchemaGeneration: "4" beside type and document/data, never inside the stored draft. For example: create_document({type: "cel_libraries", document, celSchemaGeneration: "4"}). An execute_function candidate draft also needs this argument; read-only get_cel_reference does not.

{"type":"cel_libraries","id":"LIBRARY_ID","functionName":"publish","args":{"expectedDraftRevision":1},"celSchemaGeneration":"4"}

Use the actual ID/current revision. Validation and tests accept an optional unsaved candidate. get_cel_reference with context:"library" and exact celLibraries pins returns allowed shared and imported signatures.

The authenticated public API exposes the same document actions: POST /v1/dynadata/type/cel_libraries/item/{id}/executeFunction/{functionName}. The JSON body is the action's arguments, such as expectedDraftRevision for publish. CEL-bearing HTTP writes include X-Autotalk-Cel-Schema: 4, also for nonbrowser clients. Generic library CRUD uses the normal Dynadata endpoints and permissions.

Debugging a live expression​

Team-visible errors identify the expression and where it ran. Expand the bubble, copy the expression, and inspect its section/index and diagnostic. With debug logging enabled, available variable values help build a representative fixture.

Collapsed CEL diagnostic using synthetic demo data

A collapsed demo diagnostic names the error and shows a short preview.

Expanded CEL diagnostic showing its expression and source

Expand the diagnostic to see the expression, source, section and index.

CEL Variables tab with synthetic contact and company values

The CEL Variables tab shows the values captured for the diagnostic. These demo cards illustrate a missing contact.nickname; they contain no customer records.

Fix the expression in its owning agent/workflow/form or library draft. Test it, then save; for a library, publish and deliberately update the exact caller pin. Library drafts cannot change live behavior until a caller imports a release.