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.
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.
"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
| Option | Purpose |
|---|---|
| On failure | Throw, return null, use a literal fallback string, or keep the authored expression |
| Result type | Expected application result: any, string, number, boolean, date, array or object |
| Fallback value | Literal 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 filters the current field's available functions and shows their signatures.
| Context | Typical inputs |
|---|---|
| Agent | contact, contactMessage, conversation, company, declared tool parameters |
| Workflow | _workflow, occurrenceDate, declared manual inputs and trigger-specific doc, prev, op, model |
| Form | values, item, args, company, employee when supplied by that form |
| Library function | Only 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
- Supply representative JSON variable values.
- Run with the button or
Ctrl+Enter. - Compare the value/type or inspect the diagnostic.

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.
A collapsed demo diagnostic names the error and shows a short preview.

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

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.