+
+
diff --git a/site/src/content/docs/concepts/expressions.md b/site/src/content/docs/concepts/expressions.md
index ac7e127..fea86da 100644
--- a/site/src/content/docs/concepts/expressions.md
+++ b/site/src/content/docs/concepts/expressions.md
@@ -1,90 +1,198 @@
# CEL Expressions
-Data flows between steps through [CEL (Common Expression Language)](https://github.com/google/cel-go) expressions. CEL is a small, fast, non-Turing-complete expression language designed by Google for security and policy evaluation.
+Mantle uses [CEL (Common Expression Language)](https://cel.dev) for data flow and conditional logic in workflows. CEL is a small, fast, non-Turing-complete expression language designed by Google for security and policy evaluation. It is strongly typed, sandboxed, and evaluates in nanoseconds — making it ideal for workflow orchestration. See the [CEL language spec](https://cel.dev) for the full reference.
-## Three Namespaces
+## How Mantle Uses CEL
-| Namespace | Example | Description |
-|---|---|---|
-| `inputs` | `inputs.url` | Values passed when the workflow is triggered. |
-| `steps` | `steps.fetch-data.output.body` | Output from a previously completed step. |
-| `env` | `env.API_TOKEN` | Environment variables available to the engine. |
+CEL expressions appear in two contexts inside a workflow YAML file:
-## Where CEL Appears
-
-**Conditional execution** -- the `if` field on a step:
+- **Template interpolation** in `params` values — wrapped in `{{ }}` delimiters, can be mixed with literal text.
+- **Bare expressions** in the `if` field — no `{{ }}` wrapper, evaluated as a boolean to decide whether a step runs.
```yaml
-if: "steps.fetch-data.output.status_code == 200"
+steps:
+ - name: notify
+ action: http/request
+ # Bare CEL — evaluated as boolean, no {{ }} needed
+ if: "steps.check.output.status == 200"
+ params:
+ method: POST
+ # Template CEL — embedded in a string with {{ }}
+ url: "https://api.example.com/{{ steps.lookup.output.id }}/notify"
+ body:
+ message: "Hello {{ inputs.name }}, your request is ready."
```
-The step runs only when this expression evaluates to `true`. If the expression evaluates to `false`, the step is skipped.
+## Available Variables
+
+Every CEL expression has access to four namespaces:
-**Template interpolation** -- double-brace syntax in `params`:
+| Namespace | Example | Description |
+|---|---|---|
+| `steps..output` | `steps.fetch.output.json.title` | Output from a previously completed step. |
+| `inputs.` | `inputs.url` | Values passed when the workflow is triggered. |
+| `env.` | `env.API_BASE_URL` | Environment variables (restricted to `MANTLE_ENV_*` prefix). |
+| `trigger.payload` | `trigger.payload.repository.full_name` | Webhook trigger data (server mode only). |
+
+### Step outputs
+
+Each connector populates output fields. For the HTTP connector, common fields are `status`, `headers`, `body`, and `json` (the parsed JSON body). Access them with dot or bracket notation:
```yaml
-params:
- url: "{{ inputs.url }}"
- prompt: "Summarize: {{ steps.fetch-data.output.body }}"
+# Dot notation
+summary: "{{ steps.fetch.output.json.title }}"
+
+# Bracket notation — required when step names contain hyphens
+url: "{{ steps['get-user'].output.json.profile_url }}"
```
-Template expressions are evaluated and their results are substituted into the string.
+### Workflow inputs
-## Data Flow Example
-
-Consider this workflow:
+Inputs are declared at the top of the workflow file and passed at runtime:
```yaml
inputs:
- url:
+ name:
type: string
+ count:
+ type: number
steps:
- - name: fetch-data
+ - name: greet
action: http/request
params:
- method: GET
- url: "{{ inputs.url }}"
+ url: "https://api.example.com/greet"
+ body:
+ greeting: "Hello {{ inputs.name }}"
+```
- - name: summarize
- action: ai/completion
- params:
- provider: openai
- model: gpt-4o
- prompt: "Summarize: {{ steps.fetch-data.output.body }}"
+### Environment variables
+
+Environment variables are available under `env.*`, but only those with the `MANTLE_ENV_` prefix are exposed. The prefix is stripped in the expression:
+
+```yaml
+# If MANTLE_ENV_API_BASE_URL is set:
+url: "{{ env.API_BASE_URL }}/v1/resource"
```
-The data flows like this:
+### Trigger data (webhooks)
-1. The caller provides `url` as an input when triggering the workflow.
-2. Step `fetch-data` reads `inputs.url` and makes an HTTP GET request.
-3. The HTTP connector returns output with fields like `status_code`, `headers`, and `body`.
-4. Step `summarize` reads `steps.fetch-data.output.body` to build its prompt.
-5. The AI connector returns the completion result.
+In server mode, workflows triggered by webhooks can access the incoming payload:
+
+```yaml
+repo: "{{ trigger.payload.repository.full_name }}"
+action: "{{ trigger.payload.action }}"
+```
+
+## Common Expressions
+
+### String operations
+
+```yaml
+prompt: "Hello {{ inputs.name }}"
+url: "https://api.example.com/{{ steps.lookup.output.id }}"
+message: "Status: {{ steps.check.output.json.status }}"
+```
-Each step can only reference outputs from steps that have completed before it runs. The engine detects these references automatically and treats them as implicit dependencies. When combined with explicit `depends_on` declarations, this enables parallel execution -- see [Execution Model](/docs/concepts/execution).
+### Accessing nested data
-## CEL Syntax Quick Reference
+```yaml
+# JSON response fields
+summary: "{{ steps.fetch.output.json.title }}"
+
+# Nested objects
+city: "{{ steps.fetch.output.json.address.city }}"
+
+# Array access
+first_item: "{{ steps.list.output.json.items[0] }}"
+```
-**Access step output:**
+### Conditional execution (if field)
+
+The `if` field uses bare CEL expressions — no `{{ }}` wrapper:
```yaml
-url: "{{ steps['step-name'].output.json.field }}"
+# Status code check
+if: "steps.check.output.status == 200"
+
+# Numeric comparison
+if: "steps.analyze.output.json.score > 0.8"
+
+# Check list length
+if: "size(steps.fetch.output.json.items) > 0"
+
+# Check field existence
+if: "has(steps.prev.output.json.email)"
+
+# Boolean logic
+if: "inputs.verbose == true && steps.fetch.output.status == 200"
+
+# Negation
+if: "steps.fetch.output.body.contains('error') == false"
```
-**Access inputs:**
+### String functions
```yaml
-url: "{{ inputs.field_name }}"
+if: "steps.fetch.output.json.status.startsWith('2')"
+if: "steps.data.output.json.email.contains('@company.com')"
+if: "steps.input.output.json.name.endsWith('.pdf')"
```
-**Bracket notation is required** when step names contain hyphens:
+### Size checks
+
+```yaml
+# String length
+if: "size(steps.response.output.body) < 10000"
+
+# List length
+if: "size(steps.search.output.json.results) > 0"
+```
+
+### Type conversions
+
+```yaml
+# Convert number to string for concatenation
+timeout: "{{ string(inputs.timeout_seconds) + 's' }}"
+
+# Boolean checks
+if: "steps.validate.output.json.valid == true"
+```
+
+## Template vs Bare Expressions
+
+This distinction is important and a common source of confusion:
+
+| Context | Syntax | Example |
+|---|---|---|
+| `params` values | `{{ expression }}` | `"Hello {{ inputs.name }}"` |
+| `if` field | bare expression | `"steps.check.output.status == 200"` |
+
+Template expressions (`{{ }}`) can be mixed with literal text and are substituted into the string. You can have multiple templates in a single string:
+
+```yaml
+url: "https://{{ env.API_HOST }}/users/{{ steps.lookup.output.id }}/profile"
+```
+
+Bare expressions in `if` must evaluate to a boolean. Do not wrap them in `{{ }}`:
```yaml
# Correct
+if: "steps.check.output.status == 200"
+
+# Wrong — do not use {{ }} in if
+if: "{{ steps.check.output.status == 200 }}"
+```
+
+## Bracket vs Dot Notation
+
+**Bracket notation is required** when step names contain hyphens, because CEL interprets `-` as subtraction:
+
+```yaml
+# Correct — bracket notation for hyphenated names
if: "steps['get-user'].output.status == 200"
-# Incorrect in if expressions (hyphen interpreted as subtraction)
+# Wrong — CEL reads this as steps.get minus user.output...
if: "steps.get-user.output.status == 200"
```
@@ -94,30 +202,63 @@ if: "steps.get-user.output.status == 200"
prompt: "{{ steps.summarize.output.json.summary }}"
```
-**Template strings** use `{{ }}` delimiters inside `params` values.
-
## Type Safety
-CEL is a strongly typed language. If you compare values of different types, the expression will fail at evaluation time. For example, `inputs.count > "5"` fails because you are comparing a number to a string.
+CEL is a strongly typed language. Comparing values of different types produces an evaluation error at runtime rather than silent coercion.
+
+**Common type errors and fixes:**
-## Expression Examples
+| Expression | Problem | Fix |
+|---|---|---|
+| `inputs.count > "5"` | Comparing int to string | `inputs.count > 5` |
+| `steps.a.output.status + " OK"` | Adding int to string | `string(steps.a.output.status) + " OK"` |
+| `steps.a.output.json.missing.field` | Field may not exist | `has(steps.a.output.json.missing) ? steps.a.output.json.missing.field : "default"` |
-Boolean logic:
+**Use `has()` to guard optional fields:**
```yaml
-if: "inputs.verbose == true && steps.fetch-data.output.status == 200"
+if: "has(steps.fetch.output.json.email) && steps.fetch.output.json.email.contains('@')"
```
-String operations:
+The `has()` macro checks whether a field exists without triggering a type error. Use it when a previous step might not include a field in its output.
-```yaml
-if: "steps.fetch-data.output.body.contains('error') == false"
-```
+## Data Flow Example
-Size checks:
+Consider this workflow:
```yaml
-if: "steps.summarize.output.key_points.size() > 0"
+inputs:
+ url:
+ type: string
+
+steps:
+ - name: fetch-data
+ action: http/request
+ params:
+ method: GET
+ url: "{{ inputs.url }}"
+
+ - name: summarize
+ action: ai/completion
+ params:
+ provider: openai
+ model: gpt-4o
+ prompt: "Summarize: {{ steps['fetch-data'].output.body }}"
```
-See the [Workflow Reference](/docs/workflow-reference#cel-expressions) for the full expression reference.
+The data flows like this:
+
+1. The caller provides `url` as an input when triggering the workflow.
+2. Step `fetch-data` reads `inputs.url` and makes an HTTP GET request.
+3. The HTTP connector returns output with fields like `status`, `headers`, `body`, and `json`.
+4. Step `summarize` reads `steps['fetch-data'].output.body` to build its prompt.
+5. The AI connector returns the completion result.
+
+Each step can only reference outputs from steps that have completed before it runs. The engine detects these references automatically and treats them as implicit dependencies. When combined with explicit `depends_on` declarations, this enables parallel execution — see [Execution Model](/docs/concepts/execution).
+
+## Limitations
+
+- **`env.*` is restricted** — only environment variables with the `MANTLE_ENV_` prefix are available. This prevents accidental exposure of system secrets through CEL.
+- **Secrets are NOT available in CEL** — credentials are resolved as opaque handles at connector invocation time and are never exposed as raw values in expressions. See [Secrets Management](/docs/concepts/secrets).
+- **Resource limits** — CEL evaluation is time-bounded and output-size-limited to prevent runaway expressions from affecting engine performance.
+- **Not Turing-complete** — CEL intentionally lacks loops and general recursion. It is an expression language, not a programming language.
diff --git a/site/src/content/docs/getting-started/data-passing.md b/site/src/content/docs/getting-started/data-passing.md
index 7b7b4c3..1db9fbc 100644
--- a/site/src/content/docs/getting-started/data-passing.md
+++ b/site/src/content/docs/getting-started/data-passing.md
@@ -44,7 +44,7 @@ Execution b2c3d4e5-f6a7-8901-bcde-f12345678901: completed
## CEL Expression Syntax
-Mantle uses [CEL (Common Expression Language)](https://github.com/google/cel-go) for data passing and conditional logic. The essentials:
+Mantle uses [CEL (Common Expression Language)](https://cel.dev) for data passing and conditional logic. See the [Expressions guide](/docs/concepts/expressions) for the full reference. The essentials:
- **Access step output:** `steps['step-name'].output.json.field`
- **Access inputs:** `inputs.field_name`
diff --git a/site/src/content/docs/server-guide/index.md b/site/src/content/docs/server-guide/index.md
index 3aae364..05ed4f0 100644
--- a/site/src/content/docs/server-guide/index.md
+++ b/site/src/content/docs/server-guide/index.md
@@ -66,12 +66,12 @@ steps:
- name: alert-on-failure
action: http/request
- if: "steps['check-api'].output.status_code != 200"
+ if: "steps['check-api'].output.status != 200"
params:
method: POST
url: https://hooks.slack.com/services/T00/B00/xxx
body:
- text: "API health check failed with status {{ steps.check-api.output.status_code }}"
+ text: "API health check failed with status {{ steps['check-api'].output.status }}"
```
Apply the workflow and start the server:
diff --git a/site/src/content/docs/workflow-reference/index.md b/site/src/content/docs/workflow-reference/index.md
index b42d7b3..2623c74 100644
--- a/site/src/content/docs/workflow-reference/index.md
+++ b/site/src/content/docs/workflow-reference/index.md
@@ -224,7 +224,7 @@ The timeout must be a positive duration. `0s` and negative values are invalid.
## CEL Expressions
-Mantle uses [CEL (Common Expression Language)](https://github.com/google/cel-go) for conditional logic and data access between steps. CEL expressions appear in two places:
+Mantle uses [CEL (Common Expression Language)](https://cel.dev) for conditional logic and data access between steps. See the [Expressions guide](/docs/concepts/expressions) for practical examples. CEL expressions appear in two places:
1. **`if` fields** -- determine whether a step runs
2. **Template strings in `params`** -- reference data from inputs and previous steps using `{{ expression }}` syntax
diff --git a/site/src/layouts/Base.astro b/site/src/layouts/Base.astro
index 09ff81b..bc9b557 100644
--- a/site/src/layouts/Base.astro
+++ b/site/src/layouts/Base.astro
@@ -1,6 +1,7 @@
---
import '../styles/global.css';
import { ClientRouter } from 'astro:transitions';
+import SearchModal from '../components/SearchModal.astro';
interface Props {
title: string;
@@ -37,6 +38,7 @@ const { title, description = "Define AI workflows as YAML. Deploy them like infr
Skip to content
+