Skip to content

Edge Conditions

Edge conditions decide whether an edge fires when the system is choosing which path to take next. Without a condition, an edge always fires. With a condition, the edge fires only when the condition is true.

This page covers what conditions can express today (v1), the small expression language they use, and how to read values from your agent's memory and from the conversation around it.

When you need a condition

Most edges are simple — "after stage A, go to stage B." No condition needed.

You add a condition when which edge to take depends on something the system knows. Two examples:

  • A mentoring bot has two edges leaving the "ask about the business" stage. One goes to the standard advice flow; the other goes to a short profile-collection flow if the mentee hasn't told us what kind of business they run yet. The profile edge has a condition that checks whether business_type is missing.
  • An email auto-responder has an "after-hours" edge that should only fire if the email arrived outside business hours. That edge has a time-based condition.

Multiple edges from the same source can each have their own condition; the first one whose condition is true (in the edge resolution order) fires.

What a condition looks like

A condition is a small structured expression — JSON-shaped on disk, authored in the portal as a friendly visual builder. Builders rarely see the raw JSON.

The Juno example, in plain English:

"If the mentee's business_type is missing and the chat has more than 20 messages, take this edge."

In the portal, the builder adds two clauses joined by AND:

Variable Operator Value
memory.business_type is missing
chat.message_count is greater than 20

The portal serializes that to:

{
  "and": [
    { "missing": ["memory.business_type"] },
    { ">": [{ "var": "chat.message_count" }, 20] }
  ]
}

You don't need to read or write that JSON yourself. The visual builder is the everyday surface.

What you can ask about — variable scopes

A condition can read values from five places:

Scope What it is Example
memory.<path> Data stored in the agent's memory (per-user or global). memory.business_type, memory.family.allergies
chat.<field> Facts about the current chat or session. chat.message_count, chat.started_at, chat.stage
agent.<field> The agent's configuration and state. agent.config.confidence_threshold
message.data.<field> Values the agent extracted from the message at hand. message.data.country_of_residence
now(), today() The current time and date. Use in comparisons with < and >.

A condition can mix scopes freely — for example, "if message.data.confidence is below agent.config.confidence_threshold, do X."

What you can compare — operators

These are the operators you can use in v1:

Category Operators What they mean
Comparison ==, !=, <, <=, >, >= Compare two values.
Logical and, or, ! Combine clauses, or invert one.
Membership in Is a value in a list, or a substring of a string.
Variable var Read a variable. (Use the visual builder; you almost never write this directly.)
Missing missing, missing_some Check whether one or more variables are unset.
Arithmetic +, -, *, / For numeric comparisons.

If you need something that isn't here (regex match, string concat, list mapping), let us know — these are the operators we picked for v1 because they cover the common cases. We'll expand on demand.

How memory values are looked up

Most conditions reference memory.<something>. The system resolves each path through a two-step cascade.

Step 1 — pick the layer

For a multi-user agent (e.g. Juno serving many mentees, or Know Yourself with an optional human guide):

  1. Layer A — agent default. The agent's system memory: holds conversation/stage definitions and any agent-wide defaults.
  2. Layer B — user override. The user's per-user memory. Wins over Layer A on conflict.

For a single-memory agent there is just one layer.

Step 2 — within each layer, walk up the source-node parent chain

  1. Start at the source node of the edge — the node the chat is currently at.
  2. Walk up the parent chain of that node, reading each ancestor node's data.
  3. If multiple ancestors carry the same field, the deepest node wins (closer to the source node = higher priority).

This is a CSS-style cascade. You can keep general defaults near the root of your memory and override them at specific stages or sub-conversations without copy-pasting.

Worked example — multi-user override

Juno is configured with an agent-wide default at loc: data in the agent's system memory:

threshold: 6

For one specific mentee, Juno's portal lets the mentee customize their threshold. That value lives in the mentee's per-user memory at loc: data:

threshold: 3

A conditional edge has the JSONLogic expression { "<": [ {"var": "memory.threshold"}, 5 ] }. When evaluated:

  • For that mentee: Layer B (user) supplies threshold = 3. The comparison is 3 < 5 = true. Edge fires.
  • For any other mentee with no override: Layer A (agent) supplies threshold = 6. The comparison is 6 < 5 = false. Edge does not fire.

The agent author writes the default once; per-user behavior follows without any extra wiring.

Time

now() and today() snapshot the platform's clock at the moment the condition is being evaluated. Comparisons use ISO 8601 strings, so:

{ "<": [ "{var: now()}", "{var: agent.config.cutoff}" ] }

works the same as you'd expect — string comparison gives the right answer for ISO timestamps.

There is no date arithmetic in v1 (no add_days, no since). If you need "more than 7 days ago," put the threshold somewhere your agent can compute and read — for example, store an agent.config.cutoff that your agent updates daily.

When a memory is encrypted

If your agent uses encrypted memory (the highest privacy tier — used by single-user agents like Know Yourself), and a condition needs to read a value while no decryption key is available in the active session, the edge will not silently fall through. The system raises a clear error so the user can take action.

What the user sees when this happens:

This conversation needs your password to unlock your private memory before it can continue. Please re-enter your password and try again.

The chat turn aborts (rather than firing the edge with stale or missing data, which could be misleading); the user's next action is to unlock their memory and try again. You don't have to do anything special to opt into this behavior — it's the safe default whenever the memory is encrypted.

The platform also writes a structured log entry naming the offending edge, the field it tried to read, and the memory URN — useful when an agent author is debugging conditions over encrypted memories.

Reserved names

Two field paths are reserved for a future agent-exceptions feature and cannot be used in extraction specs or referenced in conditions:

  • message.data.exception
  • message.data.exceptions

These will hold safety signals (self-harm, health crisis, prompt injection attempt, etc.) once that feature ships. The reservation keeps the door open without breaking anything.

Worked example — Pong's after-hours auto-reply

Pong (the email auto-responder) doesn't run inside a chatbot turn at all — it fires when an email arrives. Conditions on Pong's edges use the same five scopes; what changes is which ones are populated.

When an email arrives outside business hours (8am–6pm in the agent's configured timezone), reply with the after-hours template instead of the regular auto-draft.

Pong's agent-config node (loc: config in its system memory) has:

business_hours_start: "2026-04-26T08:00:00Z"
business_hours_end:   "2026-04-26T18:00:00Z"

(These get rolled forward each day by Pong's runnable.)

The "after-hours" edge has the condition (built in the portal as two clauses joined by OR):

  • message.data.received_at is less than agent.config.business_hours_start
  • message.data.received_at is greater than agent.config.business_hours_end

Saved as JSONLogic:

{
  "or": [
    { "<": [ {"var": "message.data.received_at"}, {"var": "agent.config.business_hours_start"} ] },
    { ">": [ {"var": "message.data.received_at"}, {"var": "agent.config.business_hours_end"} ] }
  ]
}

When an email arrives at 21:00, Pong's extraction spec produces message.data.received_at = "2026-04-26T21:00:00Z". The condition evaluates: 21:00 > 18:00 → true. The after-hours edge fires.

Notice that none of this involved a chat turn. Edge conditions are a platform-wide primitive; chatbots are just one of their callers.

Worked example — Juno's force-profile edge

The full Juno setup we sketched at the top:

When the mentee has been chatting for a while but still hasn't shared what kind of business they run, gently insist before giving more advice.

In the portal:

  1. On the "give advice" stage, you have your normal "continue advising" edge — no condition.
  2. You add a second edge from the same stage, pointing at the "force-profile" stage in the profile-collection conversation.
  3. On the new edge, you build a condition with two clauses, joined by AND:
  4. memory.business_type is missing
  5. chat.message_count is greater than 20
  6. Save.

Now: each time a chat turn completes at the "give advice" stage, the system checks both edges. The new edge fires only when the mentee has been chatting for more than 20 messages without telling Juno what their business is. Otherwise, the standard edge fires.

Operator reference (full v1 list)

This is every operator the platform accepts in v1. Anything outside this list is rejected at save time. The structured builder in the portal exposes the most common ones; arithmetic and the membership operator are valid via the wire format and via JSONLogic expressions embedded in the condition.

Category Operators Use
Comparison ==, !=, <, <=, >, >= Compare two values. Strings, numbers, booleans, ISO timestamps all work.
Logical and, or, ! Combine clauses, or invert one. Group rows in the builder map to and / or.
Membership in Element-in-list, or substring-in-string.
Variable var Read a variable from one of the five scopes. (Builder uses this implicitly.)
Missing missing, missing_some Test whether one or more variables are unset.
Arithmetic +, -, *, / Useful for thresholds expressed as multiples of another field (target * 0.8).

Excluded for now (will return when there's real demand): cat, substr, min, max, map, reduce, filter, if, log, regex match. If your agent needs one of these, file an issue.

Authoring through the portal builder

The portal's structured condition builder supports the most common shapes:

  • Eight curated operators (==, !=, <, <=, >, >=, in, missing).
  • AND/OR groups, nestable.
  • Literal-or-variable values on the right-hand side. Comma-separated literals for in.

Conditions that are valid JSONLogic but outside the builder's curated shape (arithmetic inside a comparison, nested expressions) currently fall back to raw JSON. Most everyday conditions land cleanly in the builder.

  • Conversation routing — how edges, stages, conversations, and goals fit together at the next level up.