Python expressions
An agent is written in JSON. The string values in that JSON can be Python expressions; a key cannot. This page explains what Search2o does with an expression, from the moment a developer validates the agent to the moment the expression runs.
What counts as an expression
A string value is an expression when the whole string is wrapped in braces.
"var": {
"total": "{ price * quantity }",
"label": "Order total",
"greeting": "{ f'Hello {sys.inputs[\"name\"]}' }"
}total is an expression. label is plain text. greeting is an expression that builds a string with an f-string.
The braces must enclose the whole value. "Hello {name}" is plain text, and the agent sees the braces literally. To mix text and values, write an f-string inside the braces.
A value that starts with { and ends with } is always read as an expression. The string '{"a": 1}' is therefore not text: that value is the Python dict {"a": 1}. To keep a leading brace literal, do not end the string with a closing brace.
An expression is one Python expression. Statements are not allowed: no assignment, no import, no def, no semicolon. The var command is how an agent sets a variable.
The result can be of any Python type: a number, a list, a dict, an object. A field that may hold either plain text or an expression is called a dynamic string in the command reference.
"var": {
"greeting": "Hello, world!", // plain text
"now": "{ datetime.now().isoformat() }", // datetime must be in the allowlist
"weather": "{ f'{city}: {temperature}' }", // f-strings are common
"squares": "{ [n * n for n in numbers] }",
"square_map": "{ {n: n * n for n in numbers} }",
"profile1": "{ await fetch_user_profile(user_id) }",
"profile2": "{ fetch_user_profile(user_id) }", // a bare async call is awaited for you
"lines": "{ [line async for line in stream_lines(url)] }"
}Where expressions can be used
Most command fields take an expression. The fields of var, if, output and api are typical. The system and user text of a prompt are expressions too.
Some fields of the profiles in the configuration also take expressions: the URL and headers of an API profile, the connection string of a database profile. Profiles are shared by every agent in the account, so a secret usually appears there, for example "{ f'Bearer {sys.secret[\"OPENAI_API_KEY\"]}' }".
Two stages
An expression is checked twice and run once.
- When the agent is validated. Search2o Cloud reads each expression and refuses the agent if an expression breaks a rule. The agent server then checks that the names and secrets the expressions use exist.
- When the agent runs. The agent server evaluates the expression in the agent runtime.
The agent server runs the version of each expression that passed the first stage. The agent runtime adds its own protection: an expression sees only the allowlist, never Python's builtins.
Stage one: validation
What the cloud refuses
The cloud parses each expression with Python's own parser. An expression that is not valid Python is refused with the line and column of the error.
The cloud then refuses these constructs:
| Construct | Example | Why |
|---|---|---|
| A name or attribute that starts with an underscore | x.__class__, obj._cache | Private and special members lead out of the agent's data. |
str.format and str.format_map | '{0.x}'.format(v) | A format string reads attributes at run time. Use an f-string instead. |
lambda | lambda x: x + 1 | An expression cannot define a function. |
| The walrus operator | (n := len(items)) | An expression cannot assign. |
| Calling the result of an expression | handlers[0](x), f()() | Only a function called by name, or a method, can be called. |
| A keyword argument that starts with an underscore | f(_x=1) | Same reason as private members. |
| Characters outside printable ASCII | 'café' | Use a Python escape. In the JSON the backslash is doubled: "{ 'caf\\u00e9' }". |
| Deep comprehensions | five nested list comprehensions | The account sets the limits. The defaults are a depth of 4 and 3 for clauses. |
| An operator the account has turned off | a << b | The account decides which operators are allowed. |
The account's settings for comprehensions and operators are under Compile rules.
Rules for sys
sys holds what the runtime provides, and can only be used as sys.<member>. An expression cannot pass sys to a function or store it in a variable.
A secret can only be read as sys.secret['NAME'], with the name written out. A key held in a variable is refused, and so is sys.secret on its own. The name of every secret an agent uses is therefore visible in the definition; the values never are.
A member of sys that does not exist is refused. sys.inputs, sys.query, sys.secret and sys.exists are always present. sys.userEmail, sys.userSession, sys.serverIp and sys.cookies are present only when an administrator enables them. See System variables.
Inside a command, command.<field> reads a field that the same command has already set. A field the command does not have is refused.
Variables that nothing sets
The cloud also refuses an expression that reads a local variable no earlier command in the function can have set: not an argument, not a loop variable, not the target of a var before it. The check is permissive: a variable set on any path before the expression, for example in one branch of an if, passes. An agent. or conv. variable passes when any var command in the agent sets it. The same check applies to the text of each prompt, at each llm command that names it.
Operator rewriting
The account can map an operator to a function. When ** is mapped to bounded_pow, the expression a ** b runs as bounded_pow(a, b). An account uses this to bound an operation such as a very large power. The function must be in the allowlist and take two values.
What the agent server checks
The cloud does not know which functions the agent server provides or which secrets it holds. The cloud therefore gives the agent server the names the expressions use and the secrets they read.
The agent server refuses the agent when a name is not in the allowlist or a secret cannot be found. Each message gives the path of the first place the name or secret is used.
Secrets from environment variables and files are stored on one machine. The check therefore tells a developer that the secret exists on the agent server that validated the agent. Another agent server in the same account must have the same secrets, and the same vault when secrets come from one.
The validation run
Validation also runs the agent once with the developer's validation query. An expression that fails on real data fails here, before the agent can be published. See Trace and validation.
Stage two: running
The names an expression can see
An expression runs inside the function that contains it. It can read:
- The function's local variables and arguments, by name.
- Agent variables, as
agent.name. - Conversation variables, as
conv.name. - The agent's const, as
const.name. sysandcommand.result, the result of the last command that produces one.exc, the error being handled inside anonErrorblock.- The names in the allowlist.
Nothing else. The allowlist takes the place of Python's builtins, so len or sorted works only when the allowlist lists it. The namespaces are described under Namespaces.
Checking whether a variable exists
Reading a variable that was never set is an error. sys.exists(scope, name) answers whether the variable exists first. The scope is one of local, agent, conv, const or allowlist.
"var": {
"agent.visits": "{ (agent.visits if sys.exists('agent', 'visits') else 0) + 1 }"
}sys.exists('allowlist', 'json') answers whether the allowlist grants json.
What the allowlist hands over
A function or class in the allowlist is the real Python object. A method on a value works as in Python, so 'abc'.upper() and items.append(x) work.
A module in the allowlist is not handed over as a module. The agent gets the module's functions, classes and constants, not the modules it imports. Granting json does not grant what json uses internally; an expression that uses such a module is refused when the line runs. List that module on its own. The entry forms are under Allowlist.
Awaiting
An expression can await an async function from the allowlist. The runtime detects the await and runs the expression as a coroutine. When the whole expression is a call to an async function, the result is awaited for you, so await is optional there. Inside a larger expression, an async call must be awaited explicitly. To run several calls at once, use the parallel command.
The value an expression produces
An expression produces any Python value. Each field expects a type, and a value of the wrong type is an error that names the type the field expected. For example, the timeout of an api command must be a whole number.
Agent variables and conversation variables are saved with the conversation, so their values must be JSON: a string, a number, a boolean, None, or a list or dict of those. A local variable can hold any value.
Errors
An expression that raises an error stops the command. The message gives the path of the command and the Python error. An onError block on the command or the function can handle the error instead. Inside that block exc holds the error.
Limits
The rules on this page decide what an expression can reach, not how much work it does. An expression runs on the agent server alongside every other agent run, so a very long computation delays the other runs. The limits that apply are the ones set in the configuration: run time, loop iterations, database rows and the operator rewrites described above. See Runtime limits.

