diff --git a/site/package-lock.json b/site/package-lock.json index 9b56845..25bc961 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -12,6 +12,7 @@ "@astrojs/sitemap": "^3.7.1", "@tailwindcss/vite": "^4.2.2", "astro": "^6.0.8", + "pagefind": "^1.4.0", "tailwindcss": "^4.2.2" } }, @@ -1239,6 +1240,84 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", + "integrity": "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz", + "integrity": "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz", + "integrity": "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz", + "integrity": "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz", + "integrity": "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz", + "integrity": "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", @@ -5195,6 +5274,23 @@ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, + "node_modules/pagefind": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.4.0.tgz", + "integrity": "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.4.0", + "@pagefind/darwin-x64": "1.4.0", + "@pagefind/freebsd-x64": "1.4.0", + "@pagefind/linux-arm64": "1.4.0", + "@pagefind/linux-x64": "1.4.0", + "@pagefind/windows-x64": "1.4.0" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", diff --git a/site/package.json b/site/package.json index 4d15a9e..fd7a1f2 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "astro dev", - "build": "astro build", + "build": "astro build && npx pagefind --site dist", "preview": "astro preview" }, "dependencies": { @@ -13,6 +13,7 @@ "@astrojs/sitemap": "^3.7.1", "@tailwindcss/vite": "^4.2.2", "astro": "^6.0.8", + "pagefind": "^1.4.0", "tailwindcss": "^4.2.2" } } diff --git a/site/src/components/DocsSearch.astro b/site/src/components/DocsSearch.astro new file mode 100644 index 0000000..3945bbb --- /dev/null +++ b/site/src/components/DocsSearch.astro @@ -0,0 +1,27 @@ +--- +// This component renders only a trigger button. +// The search modal is rendered once in Base.astro via SearchModal. +--- + + diff --git a/site/src/components/SearchModal.astro b/site/src/components/SearchModal.astro new file mode 100644 index 0000000..9ccc534 --- /dev/null +++ b/site/src/components/SearchModal.astro @@ -0,0 +1,205 @@ +--- +// Single search modal rendered once in Base.astro. +// Multiple DocsSearch trigger buttons open this shared modal. +--- + + + 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 +