This page explains the core ideas behind Mantle's design: why workflows are treated as infrastructure, how versioning works, what checkpointing guarantees (and does not guarantee), and how data flows between steps.
Mantle borrows the validate-plan-apply pattern from infrastructure tools like Terraform. Workflow definitions are not executed directly from YAML files. Instead, they go through a controlled deployment lifecycle:
mantle validate --> mantle plan --> mantle apply --> mantle run
(offline) (diff against (store new (execute
database) version) latest)
validate parses the YAML and checks it against structural rules: naming conventions, required fields, valid types, and correct durations. This runs offline with no database connection, so you can run it in a pre-commit hook or CI pipeline before anything touches the database.
plan compares your local file against the latest version stored in the database and shows a diff of what will change. Nothing is written.
apply validates the workflow, hashes the content with SHA-256, compares the hash against the latest stored version, and -- if the content changed -- inserts a new immutable version into the workflow_definitions table. If nothing changed, it reports "No changes" and does nothing.
run executes the latest applied version of a workflow, checkpointing each step to Postgres as it completes.
This lifecycle has a few important properties:
- Every version is immutable. Once applied, a version is never modified or deleted. Version 1 of a workflow always contains exactly what was applied as version 1.
- Deployments are auditable. You can trace what definition was active at any point in time by looking at version numbers and timestamps.
- Validation is separated from storage. You can validate dozens of files in CI without ever connecting to a database.
Every time you mantle apply a workflow with changed content, Mantle creates a new version with an incremented version number. The version history is strictly append-only.
mantle apply workflow.yaml # Creates version 1
# edit workflow.yaml
mantle apply workflow.yaml # Creates version 2
mantle apply workflow.yaml # No changes — still version 2
# edit workflow.yaml again
mantle apply workflow.yaml # Creates version 3
Mantle determines whether content has changed by comparing SHA-256 hashes of the raw YAML file content. If the hash matches the latest version, no new version is created. This means whitespace-only changes or comment changes do create new versions (since the raw bytes change), while applying the same file twice does not.
Each version record in the database stores:
| Column | Description |
|---|---|
id |
Unique UUID |
name |
Workflow name (e.g., fetch-and-summarize) |
version |
Integer version number, starting at 1 |
content |
The parsed workflow definition as JSON |
content_hash |
SHA-256 hash of the raw YAML file |
created_at |
Timestamp of when this version was applied |
When Mantle executes a workflow, each step's result is checkpointed to Postgres before the next step begins. If the process crashes mid-execution, it resumes from the last completed step rather than starting over.
- No duplicate internal work. A step that completed and was checkpointed before a crash is not re-executed after recovery.
- Durable state. Step outputs survive process restarts because they are stored in Postgres, not in memory.
- Crash recovery. A workflow execution can be resumed by any Mantle instance with access to the same database.
- Exactly-once external side effects. If a step makes an HTTP POST and the process crashes after the POST completes but before the checkpoint is written, the POST will be repeated on recovery. This is inherent to any system that interacts with external services. Use idempotency keys in your external APIs to handle this.
- Atomicity across steps. Each step is independent. There is no rollback of previously completed steps if a later step fails.
The database schema for execution tracking uses three tables:
workflow_executions-- one row per workflow run, tracking overall status (pending,running,completed,failed,cancelled)step_executions-- one row per step attempt, tracking status, output, and errors- Each step attempt is uniquely identified by
(execution_id, step_name, attempt), supporting retries
Data flows between steps through CEL (Common Expression Language) expressions. CEL is a small, fast, non-Turing-complete expression language designed by Google for security and policy evaluation.
| 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. |
Conditional execution -- the if field on a step:
if: "steps.fetch-data.output.status_code == 200"The step runs only when this expression evaluates to true. If the expression evaluates to false, the step is skipped.
Template interpolation -- double-brace syntax in params:
params:
url: "{{ inputs.url }}"
prompt: "Summarize: {{ steps.fetch-data.output.body }}"Template expressions are evaluated and their results are substituted into the string.
Consider this workflow:
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 }}"The data flows like this:
- The caller provides
urlas an input when triggering the workflow. - Step
fetch-datareadsinputs.urland makes an HTTP GET request. - The HTTP connector returns output with fields like
status_code,headers, andbody. - Step
summarizereadssteps.fetch-data.output.bodyto build its prompt. - 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 Parallel Execution below.
Mantle does not execute steps strictly in list order. Instead, it builds a directed acyclic graph (DAG) from both explicit depends_on declarations and implicit dependencies detected from CEL expression analysis (e.g., steps.fetch-data.output), then schedules steps for concurrent execution when their dependencies allow it.
How it works:
- Steps with no dependencies start immediately and run in parallel.
- When a step completes (or is skipped), the engine checks which downstream steps now have all dependencies resolved and starts them.
- If a step fails, all downstream steps that transitively depend on it are cancelled.
Implicit dependency detection: When you reference steps['fetch-data'].output.body in a step's params or if field, the engine automatically adds fetch-data as a dependency. You do not need to redundantly list it in depends_on.
Skipped steps count as resolved. If a step's if condition evaluates to false, it is marked as skipped. Downstream steps that depend on it are still unblocked -- they can proceed, though referencing the skipped step's output will yield an empty value.
For the full depends_on field reference and a fan-out/fan-in YAML example, see the Workflow Reference.
The ai/completion connector supports multi-turn tool use (function calling). You declare tools in the step's params, each mapping a tool name to a Mantle connector action. The engine then orchestrates a loop between the LLM and your tools:
- The engine sends the prompt (and tool definitions) to the LLM.
- The LLM responds with either a text completion or one or more tool call requests.
- For each tool call, the engine executes the corresponding connector action and collects the result.
- The tool results are appended to the conversation and sent back to the LLM.
- Steps 2-4 repeat until the LLM produces a final text response, or the configured round limit is reached.
Safety limits: The max_tool_rounds param (default: 10) caps the number of LLM-tool round trips. The max_tool_calls_per_round param (default: 10) caps how many tools the LLM can invoke in a single turn. If the round limit is exhausted, the engine makes one final call asking the LLM to respond with the information gathered so far.
Error handling: If a tool execution fails, the error message is sent back to the LLM as the tool result rather than crashing the workflow. This gives the LLM the opportunity to retry with different arguments or proceed without that tool's output.
See the Workflow Reference for the tool schema and a complete YAML example.
Connectors are the integration points between Mantle and external systems. Each connector provides one or more actions that steps can invoke.
The http/request action makes HTTP requests. It is the general-purpose connector for interacting with REST APIs, webhooks, and any HTTP endpoint.
Key design points:
- JSON request bodies are automatically serialized
- JSON response bodies are automatically parsed into structured data accessible via CEL
- You control headers, method, URL, and body through step params
See the Workflow Reference for the complete parameter and output specification.
The ai/completion action sends prompts to OpenAI-compatible chat completion APIs.
Key design points:
- BYOK (Bring Your Own Key) -- Mantle does not proxy through a hosted service. You provide your own API keys through the secrets system and reference them with the
credentialfield on your workflow step. - Structured output -- you can pass an
output_schemaparameter with a JSON Schema, and the model returns JSON conforming to that schema. The parsed result is available assteps.STEP_NAME.output.json. - Custom endpoints -- the
base_urlparameter lets you point to any OpenAI-compatible API (Azure OpenAI, Ollama, vLLM, etc.) instead of the defaulthttps://api.openai.com/v1. - Tool use (function calling) -- you can declare
toolson an AI step, each mapping to a Mantle connector action. The engine runs a multi-turn loop: the LLM requests tool calls, the engine executes them via connectors, feeds results back, and repeats until the LLM produces a final text response or configured limits (max_tool_rounds,max_tool_calls_per_round) are reached.
See the Workflow Reference for the complete parameter and output specification.
The slack/send and slack/history actions interact with the Slack Web API. They handle authentication, request formatting, and error parsing for you.
Use cases:
- Sending notifications to a team channel when a workflow succeeds or fails
- Reading recent messages from a channel to summarize or process
See the Workflow Reference for parameters and output.
The postgres/query action executes parameterized SQL against external Postgres databases. It connects per-step and disconnects afterward, keeping the connector stateless.
Use cases:
- Reading data from a reporting database to feed into an AI summarization step
- Writing workflow results back to a business database
- Running scheduled data cleanup queries
See the Workflow Reference for parameters and output.
The email/send action sends emails via SMTP. It supports plaintext and HTML content, multiple recipients, and configurable SMTP servers.
See the Workflow Reference for parameters and output.
The s3/put, s3/get, and s3/list actions interact with AWS S3 and S3-compatible storage services (MinIO, DigitalOcean Spaces, Backblaze B2). The endpoint credential field allows you to point to any S3-compatible API.
See the Workflow Reference for parameters and output.
| Action | Description |
|---|---|
http/request |
HTTP requests to any URL |
ai/completion |
LLM chat completions (OpenAI-compatible) |
slack/send |
Send Slack messages |
slack/history |
Read Slack channel history |
postgres/query |
Execute SQL on external Postgres databases |
email/send |
Send email via SMTP |
s3/put |
Upload objects to S3 |
s3/get |
Download objects from S3 |
s3/list |
List objects in S3 |
Plugins extend Mantle with third-party connector actions that run as subprocesses. This keeps the core engine isolated from external code while allowing the connector surface area to grow without modifying the Mantle binary.
A plugin is an executable binary that reads JSON from stdin and writes JSON to stdout. The engine invokes the plugin as a subprocess for each step execution, passing the action name, parameters, and credential fields as a JSON payload.
Engine Plugin Process
| |
|-- spawn subprocess --------->|
|-- write JSON to stdin ------>|
| |-- execute action
|<-- read JSON from stdout ----|
|-- process terminates ------->|
The JSON input format:
{
"action": "my-plugin/do-thing",
"params": {"key": "value"},
"credential": {"api_key": "secret"}
}The JSON output format:
{
"result": "success",
"data": {"processed": true}
}If the plugin writes to stderr or exits with a non-zero code, the step fails with the stderr content as the error message.
The plugin contract is formally defined in proto/connector.proto. While the current V1.1 implementation uses the simpler JSON stdin/stdout protocol, the protobuf definition serves as the specification for a future gRPC-based plugin protocol.
The service defines three RPCs:
- Execute -- runs the connector action with parameters and credentials
- Validate -- checks whether parameters are valid for this connector
- Describe -- returns metadata about the connector's supported actions
Plugins are stored in the .mantle/plugins/ directory. Use the CLI to manage them:
mantle plugins install ./path/to/my-plugin # Copy binary to plugin directory
mantle plugins list # List installed plugins
mantle plugins remove my-plugin # Remove a pluginSee the Plugins Guide for a complete walkthrough of writing and testing a plugin.
The workflow library lets teams publish reusable workflow templates and deploy them across environments and teams. This is Mantle's mechanism for sharing best-practice workflows without copy-pasting YAML files.
The library uses a two-step model:
-
Publish -- takes a workflow that has been
mantle apply-ed and stores it as a shared template. The template includes the workflow's name, description, and full definition. -
Deploy -- copies a shared template into a target team's workflow definitions as a new version. The deployed workflow behaves identically to one created through
mantle apply.
Team A: mantle apply daily-report.yaml
mantle library publish --workflow daily-report
Team B: mantle library list
mantle library deploy --template daily-report
Publishing the same name again updates the template. Deploying the same template again creates a new version, not a duplicate.
- Sharing standard operational workflows (health checks, data syncs) across teams
- Creating starter templates for common patterns (fetch-transform-notify)
- Distributing approved workflows in a multi-tenant environment
See the CLI Reference for command details.
Mantle provides three observability mechanisms: Prometheus metrics, an immutable audit trail, and structured JSON logging. Together, they give you visibility into what your workflows are doing, how they are performing, and who changed what.
When running in server mode (mantle serve), Mantle exposes a /metrics endpoint in Prometheus exposition format. Scrape this endpoint with Prometheus, Grafana Agent, or any compatible collector.
Exposed metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
mantle_workflow_executions_total |
Counter | workflow, status |
Total workflow executions by name and outcome. |
mantle_step_executions_total |
Counter | workflow, step, status |
Total step executions by workflow, step name, and outcome. |
mantle_step_duration_seconds |
Histogram | workflow, step, action |
Step execution duration in seconds. |
mantle_connector_duration_seconds |
Histogram | action |
Connector invocation duration in seconds. |
mantle_active_executions |
Gauge | -- | Number of currently running workflow executions. |
Every state-changing operation emits an immutable audit event to the audit_events table in Postgres. Events are append-only -- they cannot be modified or deleted.
Query audit events with the mantle audit CLI command. See the CLI Reference for filter options.
In server mode, Mantle emits structured JSON logs to stdout via Go's slog package. Each log line is a JSON object with time, level, msg, and contextual fields.
{"time":"2026-03-18T14:30:00.000Z","level":"INFO","msg":"server listening","address":":8080"}
{"time":"2026-03-18T14:30:01.000Z","level":"INFO","msg":"cron scheduler started"}
{"time":"2026-03-18T14:30:05.000Z","level":"INFO","msg":"workflow execution completed","workflow":"hello-world","execution_id":"abc123"}Configure the log level with the --log-level flag, MANTLE_LOG_LEVEL environment variable, or log.level in mantle.yaml. Levels: debug, info, warn, error.
The JSON format integrates directly with log aggregation systems like the ELK stack, Datadog, Grafana Loki, and any tool that ingests structured JSON.
Mantle treats secrets (API keys, tokens, credentials) as opaque handles that are resolved at connector invocation time. You never put raw secret values in workflow YAML. Instead, you create a named credential with mantle secrets create and reference it by name in your workflow step's credential field.
Each credential has a type that defines its schema:
| Type | Fields | Use Case |
|---|---|---|
generic |
key (required) |
General-purpose API key |
bearer |
token (required) |
Bearer token authentication |
openai |
api_key (required), org_id (optional) |
OpenAI API access |
basic |
username (required), password (required) |
HTTP Basic authentication |
Types enforce that the right fields are present when you create a credential, reducing misconfiguration errors at runtime.
When the engine reaches a step with a credential field, it resolves the credential name before invoking the connector:
- Postgres lookup -- the engine queries the credentials table, decrypts the stored fields using AES-256-GCM, and passes them to the connector.
- Environment variable fallback -- if the credential is not found in Postgres, the engine checks for an environment variable named
MANTLE_SECRET_<UPPER_NAME>(hyphens are replaced with underscores). The env var value is returned as a singlekeyfield, equivalent to agenericcredential.
The resolved credential fields are injected directly into the connector as an internal _credential parameter. They are never visible in CEL expressions, step outputs, or execution logs.
- Encrypted at rest -- credential field values are encrypted with AES-256-GCM before being written to Postgres. The encryption key is not stored in the database.
- Never in expressions -- you cannot reference
credentialdata in CEL templates orifconditions. The credential is resolved inside the connector, not in the expression engine. - Never in logs -- credential values do not appear in execution logs, step outputs, or error messages.
- Typed validation -- creating a credential validates that all required fields for the type are present.
For the full operational guide, see the Secrets Guide.
Up to this point, every concept on this page describes workflows that are triggered manually: you run mantle run and the engine executes the latest applied version. Triggers and server mode introduce automatic execution.
mantle serve starts Mantle as a long-running process. Instead of executing a single workflow and exiting, the server stays up and:
- Accepts HTTP API requests to trigger and cancel executions
- Polls for due cron triggers every 30 seconds
- Listens for incoming webhook requests
- Serves health endpoints for load balancer and Kubernetes probes
The server runs migrations automatically on startup, so you do not need a separate mantle init step in your deployment pipeline.
A cron trigger tells Mantle to start a new workflow execution on a recurring schedule. The schedule uses standard 5-field cron syntax (minute, hour, day-of-month, month, day-of-week).
triggers:
- type: cron
schedule: "*/5 * * * *"The cron scheduler is built into the mantle serve process. It polls every 30 seconds, checks which cron triggers are due, and starts new executions for them. Cron triggers have no effect when running workflows manually with mantle run -- they only fire in server mode.
A webhook trigger tells Mantle to start a new workflow execution when an HTTP POST arrives at a specific path. The request body is parsed as JSON and made available as trigger.payload in CEL expressions.
triggers:
- type: webhook
path: "/hooks/on-deploy"This is the primary way to integrate Mantle with external systems: CI pipelines, monitoring alerts, GitHub webhooks, and third-party SaaS tools can all POST to a webhook endpoint to kick off a workflow.
Triggers are managed through the same IaC lifecycle as the rest of the workflow definition. When you run mantle apply:
- Triggers defined in the YAML are registered (or updated) in the database
- Triggers that were previously registered but are no longer in the YAML are deregistered
This means the workflow YAML file is the single source of truth for trigger configuration. You do not create, update, or delete triggers separately -- they are part of the apply cycle.
# First apply: registers the cron trigger
mantle apply workflow.yaml
# Edit workflow.yaml: change schedule from */5 to */10
mantle apply workflow.yaml # Updates the trigger
# Edit workflow.yaml: remove the triggers section entirely
mantle apply workflow.yaml # Deregisters all triggers
| Use Case | Trigger Type | Why |
|---|---|---|
| Periodic data sync | Cron | Runs on a fixed schedule regardless of external events |
| Deploy notifications | Webhook | Fires in response to an external event (CI pipeline) |
| Daily report generation | Cron | Time-based, no external signal needed |
| GitHub push handler | Webhook | Event-driven, triggered by an external system |
| Scheduled cleanup | Cron | Maintenance task on a recurring schedule |
Many workflows benefit from both: a cron trigger for periodic runs and a webhook trigger for on-demand execution by external systems.
Mantle is a self-hosted platform. You control where your data lives by choosing where to deploy Postgres and the Mantle binary.
All workflow data -- inputs, outputs, step checkpoints, encrypted credentials, and audit events -- is stored in the Postgres database. There is no external data store, no telemetry sent to Anthropic or any third party, and no cloud dependency unless you configure one (e.g., cloud secret backends).
Data residency is determined entirely by where you host Postgres. Deploy Postgres in the EU, and all Mantle data resides in the EU.
Mantle's Bring Your Own Key (BYOK) model means your API keys and credentials are stored in YOUR database, encrypted with YOUR encryption key. They are not sent to a third-party service for storage or management. This is a fundamental difference from SaaS platforms that hold your credentials on their infrastructure.
While Mantle itself keeps all data in your Postgres instance, the AI connector sends prompts and receives responses from external LLM provider APIs. These API calls cross network boundaries and may cross geographic borders:
| Provider | Default Endpoint | Data Location |
|---|---|---|
| OpenAI | https://api.openai.com/v1 |
US-based (unless using Azure OpenAI regional endpoints) |
| Azure OpenAI | Configured via base_url |
Region-specific, controlled by your Azure deployment |
| AWS Bedrock | Configured via region param or aws.region config |
Region-specific (e.g., eu-west-1, us-east-1) |
| Self-hosted (Ollama, vLLM) | Configured via base_url |
Wherever you host it |
Use engine.allowed_base_urls in your configuration to restrict which AI API endpoints can be called. This prevents workflow authors from accidentally or intentionally sending data to unapproved regions or providers:
# mantle.yaml
engine:
allowed_base_urls:
- "https://bedrock-runtime.eu-west-1.amazonaws.com"
- "https://my-internal-llm.corp.example.com"Any ai/completion step that specifies a base_url not on this list is rejected at validation time.
To keep all data within the EU:
- Deploy Postgres in an EU region (e.g., AWS
eu-west-1, GCPeurope-west1, or an EU-based self-hosted server) - Use EU-region AI endpoints -- AWS Bedrock in
eu-west-1, Azure OpenAI inwesteurope, or a self-hosted model in your EU infrastructure - Restrict endpoints with
engine.allowed_base_urlsto prevent calls to US-based APIs - Deploy the Mantle binary in the same EU region to avoid cross-border traffic between the application and the database
Mantle is a single Go binary that connects to a Postgres database. Cloud secret stores are optional; plugins run as subprocesses.
+---------------------------+ +-----------+
| mantle (binary) |---->| Postgres |
| | | |
| - CLI commands | | - workflow_definitions
| - Workflow engine | | - workflow_executions
| - Built-in connectors | | - step_executions
| - Plugin manager | | - credentials
| - API server + /metrics | | - audit_events
| - Cron scheduler | | - shared_workflows
| - Webhook listener | +-----------+
| - Audit emitter |
| - Secret resolver |----> Cloud Secret Stores
| | (AWS, GCP, Azure — optional)
+---------------------------+
|
|--- spawn ---> Plugin subprocesses
(JSON stdin/stdout)
Single binary. No separate worker processes, message queues, or caches. The binary contains the CLI, the execution engine, the connectors, the plugin manager, and the API server.
Postgres for everything. Workflow definitions, execution state, step checkpoints, encrypted credentials, audit events, and shared templates all live in Postgres. This keeps the operational surface area small.
Cloud secret stores are optional. Mantle resolves credentials from Postgres first, then tries configured cloud backends (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault), and finally falls back to environment variables.
Plugins are isolated. Third-party connectors run as subprocesses with a JSON stdin/stdout protocol. They cannot access the engine's memory or database directly.
- Getting Started -- install and run your first workflow
- Workflow Reference -- complete YAML schema documentation
- CLI Reference -- every command and flag
- Configuration -- config file, env vars, and flag precedence
- Secrets Guide -- credential encryption, cloud backends, and key rotation
- Server Guide -- running Mantle as a persistent server with triggers
- Plugins Guide -- writing and managing third-party connector plugins
- Observability Guide -- Prometheus metrics, audit trail, and structured logging