Tantra is a self-hosted AI agent orchestration platform. Draw workflows on a visual canvas, connect nodes that call LLMs, run ReAct agent loops, retrieve from your knowledge base, and hit external APIs — then schedule them, deploy them as REST endpoints, and expose them as MCP tools. Every run is traced at the node level, recoverable across worker restarts, and scoped to isolated workspaces.
No LangChain. No LlamaIndex. The DAG executor, ReAct loop, chunking pipeline, and retrieval are written directly against provider SDKs — that is the engineering point.
| Capability | What it means in practice |
|---|---|
| Visual DAG canvas | Draw workflows by dragging and wiring nodes — no YAML, no code required to compose |
| ReAct agent loop | Bounded step + token budgets; real HTTP tool dispatch per ACTION: step; tolerant reply parsing |
| RAG pipeline | PDF / TXT / Markdown upload → sentence chunking → Gemini embeddings (1536-dim HNSW) → hybrid retrieval |
| MCP client | Any registered MCP server's tools appear in the node library and execute with response caching |
| MCP server | Any deployed workflow is consumable as an MCP tool from Claude, Cursor, or any MCP client |
| Deploy as API | One click → versioned REST endpoint with API-key auth, scoped per workspace |
| Cron scheduling | QStash-powered tick fan-out; 30-min minimum granularity; documented constraint |
| Durable queue | FOR UPDATE SKIP LOCKED PostgreSQL queue; lease heartbeats; bounded retry; crash recovery |
| Secrets vault | AES-256-GCM encryption; workspace_id as AAD — cross-tenant decryption fails cryptographically |
| Multi-tenancy | Pooled DB with Row-Level Security; two-layer enforcement (app + Postgres RLS) |
| SSE streaming | Real-time run events pushed to the frontend as they happen; streams close on terminal status |
| AI canvas builder | Chat with an assistant that edits the current workflow graph in-place |
The canvas is built on React Flow (@xyflow/react). Each node type has a distinct coloured icon tile; the config inspector lives in a right-hand panel — clicking a node selects it, never expands it inline. Keyboard shortcuts, undo/redo history, and a floating zoom toolbar are wired in. The AI assistant panel docks into the same right-hand rail and edits the graph live via the API.
The node palette lists every available node type grouped by category — triggers, AI, data, control flow, integrations. The search bar filters the list instantly as you type.
Tantra ships 15+ built-in node types:
| Category | Nodes |
|---|---|
| AI & Agents | agent (ReAct loop) · llm (single LLM call) |
| Data & Retrieval | rag_retrieve · transform · filter · merge |
| Control flow | conditional · switch · loop · wait |
| Integrations | http_tool · mcp_tool · integration (Gmail / GitHub / Slack via Composio or Clerk OAuth) · browser |
| Triggers | manual_trigger · webhook_trigger |
| Agent-to-Agent | a2a delegate (stretch goal) |
The workflows page lets you create, filter (All / Active / Idle), run, edit, and delete workflows. Import from the template gallery with one click — templates are ordinary workflows pushed through the same public API.
Upload PDF, TXT, or Markdown files into a named knowledge base. The ingestion pipeline chunks by sentence, generates 1536-dimensional Gemini embeddings, and stores them in pgvector with an HNSW index. Retrieval uses cosine similarity with an optional BM25 re-rank pass.
From the Operations tab you schedule any workflow on a cron cadence (QStash-powered, 30-min minimum) and deploy workflows as versioned REST endpoints. Each deployment gets a unique URL and an API key scoped to the workspace.
The settings page manages account info, workspace membership, and third-party provider credentials (Groq, Gemini, OpenAI). Credentials are stored encrypted (AES-256-GCM) and never returned in API responses.
┌─────────────────────────────────────────────────────────────────┐
│ React Frontend (Vite + TypeScript) │
│ React Flow Canvas · TanStack Query · SSE streams │
└──────────────────────────┬──────────────────────────────────────┘
│ REST + SSE
┌───────────────▼──────────────────┐
│ Engine (Python 3.12 / FastAPI) │
│ DAG executor · ReAct agent loop │
│ RAG · MCP client+server · Vault │
│ Alembic migrations · Clerk / JWT │
└──────┬───────────────────┬────────┘
asyncpg │ │ HTTP (internal)
┌────────────────▼──────┐ ┌────────▼───────────────────────┐
│ PostgreSQL 16 │ │ Scheduler (Java 21 / Spring) │
│ + pgvector (HNSW) │ │ QStash tick → cron fan-out │
│ Durable queue (RLS) │ │ /tick · /health · /webhooks │
└────────────────────────┘ └────────────────────────────────┘
▲ atomic claim + lease
┌────────┴──────────────────────────────────────────────────────┐
│ Worker pool (4 concurrent) · heartbeat · retry · recovery │
└───────────────────────────────────────────────────────────────┘
| Component | Language / Framework | Deployed on | Port |
|---|---|---|---|
| Engine | Python 3.12 / FastAPI | Hugging Face Spaces (free CPU) | 7860 |
| Scheduler | Java 21 / Spring Boot 3 | Render free tier | 8080 |
| Worker | Python 3.12 (in-process) | Same container as engine | — |
| Frontend | React 19 / Vite / TypeScript | Vercel Hobby | 3000 (local) |
| Database | PostgreSQL 16 + pgvector | Supabase free | 5432 |
| Queue clock | QStash schedule | Upstash free | — |
User triggers run
│
▼
POST /api/v1/workflows/:id/run
│ Engine validates the graph (Pydantic)
│ Enqueues a Run row (status=queued, dedup key)
│
▼
Worker polls: SELECT ... FOR UPDATE SKIP LOCKED
│ Claims the run, sets lease_expires_at
│
▼
DAG executor (Kahn topological sort — deterministic)
│ For each node in declaration order:
│ looks up the registered node handler
│ passes upstream output as node input
│ captures StepTrace {status, output, tokens, duration}
│ on failure: marks run FAILED, skips all downstream nodes
│
▼
RunEvents appended (sequence-locked INSERT)
│ SSE endpoint streams events to frontend in real time
│
▼
Run reaches terminal status (success / failed / cancelled)
└── Evaluation suites advance if linked test cases exist
The agent node is a bounded ReAct-style reasoning loop written directly against Groq / Gemini SDKs — no framework wrapper.
while steps < max_steps and tokens < token_budget:
prompt = build_prompt(history, tools)
reply = call_llm(model, prompt) # streaming, 5-second timeout
action = parse_action(reply) # tolerant regex; handles malformed output
if action.type == "FINAL_ANSWER":
break
tool_result = dispatch_tool(action) # real HTTP call or MCP RPC
history.append((action, tool_result))
steps += 1
Each iteration appends a StepTrace. If the LLM produces a malformed ACTION: block the loop retries with an explicit correction prompt before giving up.
Upload (PDF / TXT / Markdown)
│
▼ text extraction (pdfminer.six · plain read)
│
▼ sentence chunking (target ≈ 512 tokens; sentence-boundary aware)
│
▼ embedding (Gemini text-embedding-004 · 1536 dim · batch of 100)
│
▼ pgvector INSERT (cosine metric · HNSW m=16 ef=200)
│
▼ at query time: top-k cosine → optional BM25 re-rank → context window
The pipeline runs synchronously in the upload handler. No background job or message queue is needed for typical document sizes.
Tantra is simultaneously an MCP client and an MCP server.
As a client: Add any stdio or SSE MCP server in Settings → Credentials. Its tools are discovered at startup and surfaced as mcp_tool nodes in the canvas. Tool results are cached per-run to avoid duplicate external calls.
As a server: Deploy any workflow from the Operations tab. The resulting endpoint speaks the MCP protocol — any MCP client can call it by passing the endpoint URL and API key as server config.
Every credential stored by Tantra is encrypted with AES-256-GCM before it reaches the database:
key_material = PBKDF2HMAC(master_key, salt=workspace_id, iterations=600_000)
ciphertext = AES256GCM.encrypt(plaintext, aad=workspace_id)The workspace_id is used as Additional Authenticated Data (AAD). A ciphertext from workspace A will fail authentication if decrypted with workspace B's key — cross-tenant decryption is cryptographically impossible, not just access-controlled.
- Docker & Docker Compose
- Node 20+
- Python 3.12+ with
uv - Java 21+ (scheduler only)
# 1. Clone
git clone https://github.com/Iamsujithd/tantra.git
cd tantra
# 2. Copy env files and fill in your keys
cp engine/.env.example engine/.env
cp frontend/.env.example frontend/.env
# 3. Start Postgres + pgvector
docker compose up -d db
# 4. Run migrations
cd engine && uv run alembic upgrade head && cd ..
# 5. Start the engine
cd engine && uv run uvicorn app.main:app --reload --port 7860
# 6. Start the frontend (new terminal)
cd frontend && npm install && npm run devOpen http://localhost:5173 — create an account and start building.
| Variable | Component | Purpose |
|---|---|---|
DATABASE_URL |
Engine | asyncpg connection string |
SECRET_KEY |
Engine | JWT signing + vault master key |
CLERK_SECRET_KEY |
Engine | Auth (alternative to plain JWT) |
GEMINI_API_KEY |
Engine | Default embedding model |
GROQ_API_KEY |
Engine | Default LLM (can be overridden per workspace) |
QSTASH_TOKEN |
Scheduler | Schedule tick delivery |
QSTASH_CURRENT_SIGNING_KEY |
Scheduler | Validate incoming ticks |
ENGINE_BASE_URL |
Scheduler | Internal call-back URL |
VITE_API_BASE_URL |
Frontend | Engine API origin |
tantra/
├── engine/ # Python 3.12 / FastAPI
│ ├── app/
│ │ ├── api/ # Route handlers (workflows, runs, kb, mcp, …)
│ │ ├── core/ # Config, security, DB session
│ │ ├── models/ # SQLAlchemy ORM models
│ │ ├── schemas/ # Pydantic request / response models
│ │ ├── services/
│ │ │ ├── dag/ # DAG executor + node registry
│ │ │ ├── agents/ # ReAct loop, tool dispatcher
│ │ │ ├── rag/ # Chunking, embedding, retrieval
│ │ │ └── mcp/ # MCP client + server transport
│ │ └── worker/ # Durable queue poller + lease heartbeat
│ └── alembic/ # DB migrations
│
├── scheduler/ # Java 21 / Spring Boot 3
│ └── src/main/java/
│ └── ai/tantra/scheduler/
│ ├── controller/ # /tick /health /webhooks
│ └── service/ # QStash signing + cron fan-out
│
├── frontend/ # React 19 / Vite / TypeScript
│ └── src/
│ ├── components/ # Reusable UI primitives
│ ├── pages/ # Route-level page components
│ ├── lib/ # API client, canvas utilities, Zustand stores
│ └── nodes/ # React Flow custom node definitions
│
└── docs/
└── assets/ # README screenshots
Why no LangChain? Provider SDKs are used directly. This makes the token budget, retry logic, and streaming behaviour fully transparent and testable. LangChain's abstractions hide these details in ways that complicate debugging at the DAG level.
Why Postgres for the queue? Eliminating Redis as an operational dependency reduces the deployment surface. FOR UPDATE SKIP LOCKED gives exactly-once claim semantics that match a hosted Postgres tier (Supabase) without additional infrastructure.
Why Java for the scheduler? QStash's signing library ships a first-class Java SDK. The scheduler is stateless, speaks plain HTTP, and can run on any free-tier JVM host without needing the Python runtime — keeping the engine container focused on execution.
Why pgvector instead of a dedicated vector DB? Documents live in the same Postgres instance as workflow metadata. A rag_retrieve node can join vector search results against workspace-scoped metadata in a single query, with RLS enforcing tenant isolation automatically.
- Cron scheduling has a 30-minute minimum granularity imposed by QStash's free tier. The scheduler documents this explicitly.
- The canvas renders up to ~200 nodes before React Flow performance degrades. Large DAGs should be split into sub-workflows connected via
a2adelegate nodes. - The free Hugging Face Spaces CPU tier cold-starts in ~30 seconds. A paid GPU Space or self-hosted deployment eliminates this.
MIT — see LICENSE.





