Kestra vs Airflow: Which Should Your AI Agent Trigger?
Kestra 2.0 vs Airflow 3.3 for AI agents: built-in MCP server and opt-in tools vs a REST API you guard yourself. Auth, permissions, approvals and pricing compared.
Sooner or later your agent needs to do something: rerun last night's import, rebuild a report, backfill a table. The lazy answer is a shell tool and a hopeful system prompt. The better answer is to let it press a button you already built: a workflow with retries, logs, alerts and an owner. You probably have an orchestrator for that already, or you're about to pick one.
So the question isn't "which orchestrator is better" in general. It's narrower: which one makes a safer, easier target for an AI agent? I compared Kestra 2.0 and Airflow 3.3 on that one question, using each project's current docs and API spec.
The Short Answer
- Pick Kestra if agents are a first-class caller. Its MCP server ships in the open-source edition, and a flow only becomes a tool when you mark it as one.
- Pick Airflow if your pipelines are already Python DAGs and your team lives in them. Agents can trigger runs through the REST API, but you build the guardrails yourself.
Side by Side
| Question | Kestra 2.0 | Airflow 3.3 |
|---|---|---|
| Workflows written in | YAML (scripts in any language as tasks) | Python DAGs (Java and Go task SDK is experimental) |
| License | Apache-2.0, paid Enterprise edition | Apache-2.0 |
| MCP server | Built in, open-source edition | Not from the Apache project; Astronomer's astro-airflow-mcp wraps the REST API |
| What the agent can see | Only flows with an McpToolTrigger | Whatever the API user can reach |
| Auth for agents | Basic auth in open source (API tokens and OAuth are Enterprise) | JWT from /auth/token |
| Per-workflow permissions | RBAC is Enterprise only | Per-DAG access with the FAB auth manager |
| Human approval step | Pause task | ApprovalOperator (3.1+, deferrable) |
| On Elestio | From $16/month (2 CPU / 4 GB) | 4 CPU / 8 GB recommended, $29/month |
Kestra: The Allowlist Is the Default
Here's what sold me. Kestra provisions a default MCP server for every tenant, but a flow only appears on it when it carries an McpToolTrigger. The flow's inputs become the tool's JSON schema, and its outputs come back in the tool response. Everything else stays invisible to the agent.
id: rerun_daily_import
namespace: company.data
inputs:
- id: day
type: DATE
description: The day to re-import, for example 2026-10-19.
tasks:
- id: import
type: io.kestra.plugin.scripts.shell.Commands
commands:
- ./import.sh "{{ inputs.day }}"
namespaceFiles:
enabled: true
outputs:
- id: day
type: STRING
value: "{{ inputs.day }}"
triggers:
- id: mcp
type: io.kestra.plugin.core.trigger.McpToolTrigger
toolName: rerun_daily_import
title: Re-run the daily import
toolDescription: Re-imports one day of orders. Safe to repeat for the same day.
mcpServer: default
annotations:
readOnly: false
destructive: false
idempotent: true
The agent gets one typed tool that takes a date, and every call becomes a normal execution tagged system.from:mcp, with retries, logs and alerts like any other run. The shell command lives inside the flow (upload import.sh as a namespace file in the company.data namespace), where you reviewed it, not in the agent's hands.
One gap: Kestra's docs say outputs come back in the tool response, but not how long a tool call waits for a slow flow. Keep agent-facing flows short, or have them start the long job with a Subflow task set to wait: false and return right away.
Connect Claude Code using the server URL shown in the server's Connect tab:
export KESTRA_MCP_URL="paste the URL from the Connect tab"
claude mcp add kestra "$KESTRA_MCP_URL" \
--transport http \
--header "Authorization: Basic $(echo -n 'admin@example.com:YOUR_PASSWORD' | base64)"
The catch: open-source Kestra has a single Basic auth login and no RBAC. The credentials in your agent's MCP header are the same ones that open the UI with full rights to edit any flow. Treat them like the admin password they are: lock Kestra to known IPs with the Elestio firewall, and change the password when an agent setup changes hands.
Airflow: A Solid API, Bring Your Own Guardrails
Airflow 3 has a clean public REST API. Get a token, then trigger a run:
TOKEN=$(curl -s -X POST https://your-airflow-domain/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "agent", "password": "YOUR_PASSWORD"}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
curl -s -X POST https://your-airflow-domain/api/v2/dags/daily_import/dagRuns \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"logical_date": null, "conf": {"day": "2026-10-19"}}'
logical_date is required in the body but can be null, which means "now". Give the DAG typed params so a malformed conf fails validation before any task runs, instead of crashing halfway through.
For MCP, Astronomer's server (uvx astro-airflow-mcp --transport stdio, configured with AIRFLOW_API_URL plus a username and password or a token) covers DAG management, triggering, logs and health. That's a lot of surface. The agent sees every DAG its API user can see, so the real guardrail is the user: with the FAB auth manager you can give it a role limited to specific DAGs. If you can't, wrap the two calls above in a tool that only accepts the DAG IDs you list.
What Airflow does better: its ecosystem is huge, the 3.3 task state store lets a retried task resume instead of starting over, and the ApprovalOperator is a clean way to make an agent-triggered run wait for a human click without holding a worker.
Who Should Pick What
- New stack, agents in the picture: Kestra. Opt-in tools and typed inputs out of the box save you writing a wrapper.
- Existing Airflow with years of DAGs: stay. Add a dedicated agent user, a scoped role and an approval task in front of anything destructive.
- You need per-team permissions on agent tools without paying: Airflow with FAB roles, since Kestra's RBAC is Enterprise only.
Troubleshooting
Kestra: the tool doesn't show up. The flow needs an McpToolTrigger, and its mcpServer must match the server you connected to. Check the server's Tool Flows tab.
Kestra: 401 from Claude Code. The header must be base64 of user:password. Run claude mcp add and start Claude Code from the same shell session, as Kestra's docs recommend.
Airflow: the run is created but never starts. New DAGs are paused by default. Unpause it in the UI or with a PATCH /api/v2/dags/daily_import that sets {"is_paused": false}.
Airflow: 422 on trigger. The body is missing logical_date. Send it, even as null.
Airflow: 401 after a while. The JWT expired. Request a new token instead of storing one forever in the agent's config.
Wrapping Up
Both are solid orchestrators. Kestra was clearly designed with agent callers in mind; Airflow gives you the parts and lets you assemble them. Deploy Kestra or Airflow on Elestio and wire up one flow before you hand your agent anything bigger. If your agent also scrapes the web, our Maxun + MCP guide covers the allowlist pattern for robots.
Thanks for reading ❤️ See you in the next one 👋