After completing this guide, you will have Mantle installed, a Postgres database running, and your first workflow executing -- all in under five minutes. From there, the guide progressively introduces data passing, conditional logic, AI/LLM integration, server mode, and multi-tenancy.
Mantle is a headless AI workflow automation platform. You define workflows as YAML, deploy them through an infrastructure-as-code lifecycle (validate, plan, apply), and execute them against a Postgres-backed engine. It ships as a single Go binary -- bring your own API keys, bring your own database, no hosted runtime required.
You need the following installed on your machine:
- Go 1.25+ -- install instructions
- Docker and Docker Compose -- install instructions
- Make -- included on macOS and most Linux distributions
Verify your setup:
go version # go1.25 or later
docker --versionClone the repository, start Postgres, build the binary, and run migrations:
git clone https://github.com/dvflw/mantle.git && cd mantle
docker compose up -d
make build
./mantle initThe docker compose up -d command starts Postgres 16 on localhost:5432 with user mantle, password mantle, and database mantle. The make build command produces a single mantle binary in the project root. The mantle init command creates all required database tables.
The default database URL uses sslmode=prefer, which tries TLS and falls back gracefully for local development. For production, always use sslmode=require or sslmode=verify-full:
export MANTLE_DATABASE_URL="postgres://mantle:secret@db.example.com:5432/mantle?sslmode=require"See Configuration for all database options.
You should see:
Running migrations...
Migrations complete.
Optionally, move the binary onto your PATH:
sudo mv mantle /usr/local/bin/Verify it works:
mantle version
# mantle v0.1.0 (791fa83, built 2026-03-18T00:00:00Z)The examples/ directory includes several ready-to-run workflows. Start with the simplest one -- a single HTTP GET request.
Look at examples/hello-world.yaml:
name: hello-world
description: Fetch a random fact from a public API — the simplest possible Mantle workflow
steps:
- name: fetch
action: http/request
params:
method: GET
url: "https://jsonplaceholder.typicode.com/posts/1"This workflow has one step: it sends a GET request to the JSONPlaceholder API and returns the response.
Check the workflow for structural errors. This runs offline -- no database connection required:
mantle validate examples/hello-world.yamlOutput:
hello-world.yaml: valid
If there are errors, Mantle reports them with file, line, and column numbers:
bad-workflow.yaml:1:1: error: name must match ^[a-z][a-z0-9-]*$ (name)
Store the workflow definition as a new immutable version in the database:
mantle apply examples/hello-world.yamlOutput:
Applied hello-world version 1
Every time you edit a workflow and re-apply, Mantle creates a new version. If the content has not changed, it tells you:
No changes — hello-world is already at version 1
You can also preview what will change before applying:
mantle plan examples/hello-world.yamlNo changes — hello-world is at version 1
Execute the workflow by name:
mantle run hello-worldOutput:
Running hello-world (version 1)...
Execution a1b2c3d4-e5f6-7890-abcd-ef1234567890: completed
fetch: completed
Inspect the execution with the execution ID from the previous step:
mantle logs a1b2c3d4-e5f6-7890-abcd-ef1234567890Output:
Execution: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Workflow: hello-world (version 1)
Status: completed
Started: 2026-03-18T14:30:00Z
Completed: 2026-03-18T14:30:01Z
Duration: 1.042s
Steps:
fetch completed (1.0s)
If a step fails, the error appears below the step name:
Steps:
fetch failed (0.5s)
error: http/request: GET https://jsonplaceholder.typicode.com/posts/1: connection refused
You can also get a quick status summary with mantle status <execution-id>.
Workflows become powerful when steps pass data to each other. Look at examples/chained-requests.yaml:
name: chained-requests
description: >
Fetch a user from a public API, then fetch their posts using the user's ID.
Demonstrates CEL data passing between steps via steps.<name>.output.
steps:
- name: get-user
action: http/request
params:
method: GET
url: "https://jsonplaceholder.typicode.com/users/1"
- name: get-user-posts
action: http/request
params:
method: GET
url: "https://jsonplaceholder.typicode.com/posts?userId={{ steps['get-user'].output.json.id }}"The key line is the second step's URL. The expression {{ steps['get-user'].output.json.id }} reads the JSON response from the get-user step and extracts the id field.
Apply and run it:
mantle apply examples/chained-requests.yaml
mantle run chained-requestsRunning chained-requests (version 1)...
Execution b2c3d4e5-f6a7-8901-bcde-f12345678901: completed
get-user: completed
get-user-posts: completed
Mantle uses CEL (Common Expression Language) for data passing and conditional logic. The essentials:
- Access step output:
steps['step-name'].output.json.field - Access inputs:
inputs.field_name - Bracket notation is required when step names contain hyphens:
steps['get-user'](notsteps.get-user) - Dot notation works for step names without hyphens:
steps.summarize.output.json.summary - Template strings use
{{ }}delimiters insideparamsvalues
Steps can run conditionally based on the output of previous steps. Look at examples/conditional-workflow.yaml:
name: conditional-workflow
description: >
Fetch todos for a user, then conditionally post a summary only if there are
incomplete todos. Demonstrates conditional execution with if: and retry policies.
inputs:
user_id:
type: string
description: JSONPlaceholder user ID (1-10)
steps:
- name: get-todos
action: http/request
timeout: "10s"
retry:
max_attempts: 3
backoff: exponential
params:
method: GET
url: "https://jsonplaceholder.typicode.com/todos?userId={{ inputs.user_id }}"
- name: post-summary
action: http/request
if: "steps['get-todos'].output.status == 200"
params:
method: POST
url: "https://jsonplaceholder.typicode.com/posts"
headers:
Content-Type: "application/json"
body:
title: "Todo summary"
body: "Fetched todos for user {{ inputs.user_id }}"
userId: "{{ inputs.user_id }}"This workflow introduces three features:
inputs-- the workflow declares auser_idinput, passed at runtime with--inputif-- thepost-summarystep only runs when the CEL expression evaluates to trueretryandtimeout-- theget-todosstep retries up to 3 times with exponential backoff and times out after 10 seconds
Apply and run it:
mantle apply examples/conditional-workflow.yaml
mantle run conditional-workflow --input user_id=3Running conditional-workflow (version 1)...
Execution c3d4e5f6-a7b8-9012-cdef-123456789012: completed
get-todos: completed
post-summary: completed
You can pass multiple inputs by repeating the --input flag:
mantle run my-workflow --input key1=value1 --input key2=value2Mantle includes a built-in AI connector that supports OpenAI-compatible APIs. Before using it, you need to store your API key as an encrypted credential.
Generate an encryption key and export it:
export MANTLE_ENCRYPTION_KEY=$(openssl rand -hex 32)Store your OpenAI API key:
mantle secrets create --name openai --type openai --field api_key=sk-proj-your-key-hereCreated credential "openai" (type: openai)
The credential is encrypted at rest with AES-256-GCM. The raw API key is never stored in plaintext, never exposed in logs, and never available in CEL expressions. See the Secrets Guide for credential types and the full security model.
Here is a workflow that fetches a webpage and uses an LLM to extract structured data (from examples/ai-structured-extraction.yaml):
name: ai-structured-extraction
description: >
Fetch a webpage and use an LLM with output_schema to extract structured data
(title, author, key topics). Demonstrates enforcing JSON structure from AI output.
inputs:
url:
type: string
description: URL of the page to fetch and extract data from
steps:
- name: fetch-page
action: http/request
timeout: "15s"
retry:
max_attempts: 2
backoff: exponential
params:
method: GET
url: "{{ inputs.url }}"
- name: extract-metadata
action: ai/completion
credential: openai
params:
model: gpt-4o
system_prompt: >
You are a structured data extraction engine. Given raw page content,
extract the requested fields accurately. If a field cannot be determined,
use null or an empty value as appropriate.
prompt: >
Extract the following metadata from this page content:
{{ steps['fetch-page'].output.body }}
output_schema:
type: object
properties:
title:
type: string
author:
type: string
key_topics:
type: array
items:
type: string
required:
- title
- author
- key_topics
additionalProperties: falseThe credential: openai field tells the engine to resolve the openai credential you created earlier. The output_schema field enforces structured JSON output from the model -- the response is guaranteed to match the schema.
Apply and run it:
mantle apply examples/ai-structured-extraction.yaml
mantle run ai-structured-extraction --input url=https://example.comRunning ai-structured-extraction (version 1)...
Execution d4e5f6a7-b8c9-0123-defa-234567890123: completed
fetch-page: completed
extract-metadata: completed
| Field | Description |
|---|---|
action |
ai/completion for chat completions |
credential |
Name of a stored credential (type openai) |
model |
Model name (e.g., gpt-4o, gpt-4o-mini) |
prompt |
User message -- supports CEL template expressions |
system_prompt |
Optional system message to set model behavior |
output_schema |
Optional JSON Schema -- enforces structured output |
So far you have been running workflows manually with mantle run. In production, you start Mantle as a persistent server that supports cron schedules and webhook triggers.
Add a triggers section to your workflow YAML:
name: api-health-check
description: Check API health hourly and on demand
triggers:
- type: cron
schedule: "0 * * * *"
- type: webhook
path: "/hooks/api-health-check"
steps:
- name: check-api
action: http/request
timeout: "10s"
retry:
max_attempts: 3
backoff: exponential
params:
method: GET
url: https://api.example.com/health
- name: alert-on-failure
action: http/request
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 }}"Apply the workflow, then start the server:
mantle apply api-health-check.yaml
mantle serveRunning migrations...
Migrations complete.
Starting server on :8080
Cron scheduler started (poll interval: 30s)
The server runs migrations on startup, starts the HTTP API on :8080, and polls for due cron triggers every 30 seconds. The api-health-check workflow now runs every hour automatically.
Send a POST request to the webhook path:
curl -X POST http://localhost:8080/hooks/api-health-check \
-H "Content-Type: application/json" \
-d '{"reason": "manual check"}'The request body is available as trigger.payload in CEL expressions within the workflow.
The server also exposes a REST API for programmatic access:
# Trigger a workflow
curl -s -X POST http://localhost:8080/api/v1/run/api-health-check | jq .{
"execution_id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"workflow": "api-health-check",
"version": 1
}# Cancel a running execution
curl -s -X POST http://localhost:8080/api/v1/cancel/e5f6a7b8-c9d0-1234-efab-345678901234Health endpoints are available at /healthz (liveness) and /readyz (readiness, checks database connectivity). See the Server Guide for production deployment, Helm chart configuration, and graceful shutdown behavior.
Mantle supports teams, users, roles, and API keys for multi-tenant environments.
Create a team, add a user, and generate an API key:
mantle teams create --name acme-corpCreated team acme-corp (id: f6a7b8c9-d0e1-2345-fabc-456789012345)
mantle users create --email alice@acme.com --name "Alice Chen" --team acme-corp --role adminCreated user alice@acme.com (role: admin, team: acme-corp)
mantle users api-key --email alice@acme.com --key-name productionAPI Key: mk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
Save this key — it cannot be retrieved again.
Key prefix for reference: mk_a1b2c3
Available roles are admin, team_owner, and operator. API keys use the mk_ prefix and are hashed before storage -- the raw key is only shown once at creation time.
This is a brief overview. Multi-tenancy, role-based access control, and team scoping are covered in detail in the CLI Reference.
You have gone from zero to running workflows with data passing, conditional logic, AI integration, server mode, and multi-tenancy. Here is where to go next:
- Workflow Reference -- complete YAML schema: every field, every validation rule, every connector (HTTP, AI, Slack, Postgres, Email, S3)
- CLI Reference -- every command, flag, and the REST API
- Secrets Guide -- credential types, encryption setup, cloud backends (AWS, GCP, Azure), and key rotation
- Server Guide -- production deployment, Helm chart, cron and webhook triggers, REST API
- Concepts -- architecture, checkpointing, CEL expressions, versioning, connectors, plugins, and observability
- Plugins Guide -- extend Mantle with third-party connector plugins
- Observability Guide -- Prometheus metrics, audit trail, and structured logging
- Configuration -- config file, environment variables, cloud backends, and flag precedence
- examples/ -- ready-to-run workflow files covering HTTP, AI, chained requests, and more