Backend repository for Draft'n Run β an AI workflow platform. Build, deploy, and monitor DAG-based pipelines of AI components.
ββββββββββββββββββββ
β back-office/ β
β (Vue frontend) β
ββββββββββ¬ββββββββββ
β Supabase JWT
βββββββββββββ΄ββββββββββββ
βΌ βΌ
βββββββββββββββββ ββββββββββββββββ
β ada-api β β Supabase β
β (FastAPI) β β (Auth, Orgs) β
β Port 8000 β ββββββββββββββββ
β + run worker β β²
ββββ¬βββββ¬ββββ¬ββββ β
β β β βββββββββ΄ββββββββ
β β β β ada-mcp β
β β β β (MCP Server) β
β β β β Port 8090 β
β β β βββββββββββββββββ
β β β
βΌ βΌ βΌ
Redis S3 PostgreSQL Qdrant Nango
| Service | Dockerfile | Port | Description |
|---|---|---|---|
| API Server | Dockerfile.api |
8000 | FastAPI + Gunicorn, includes run queue worker thread |
| Scheduler | Dockerfile.scheduler |
β | APScheduler cron jobs |
| Webhook Worker | Dockerfile.webhook-worker |
β | Redis Stream consumer for webhook events |
| Ingestion Worker | β | β | Redis Stream consumer for data ingestion |
| MCP Server | mcp_server/Dockerfile |
8090 | Standalone MCP interface for Cursor/Claude |
ada_backend/β API server (routers, services, repositories, models). READMEengine/β Graph execution engine (GraphRunner, components, field expressions)mcp_server/β MCP server (standalone, wraps the API). READMEdata_ingestion/β Document ingestion logicworkers/β Ingestion + webhook worker processesinfra/k8s/β Kubernetes manifests (Kustomize)scripts/β Dev/test scriptstests/β Test suite
The MCP server at mcp_server/ provides a Model Context Protocol interface to Draft'n Run. It runs as a standalone Kubernetes pod, authenticates via Supabase OAuth 2.1, and wraps 107 tools across 16 domains.
- Endpoint:
https://mcp.draftnrun.com(prod) /https://mcp-staging.draftnrun.com(staging) - Transport: Streamable HTTP
- Auth: Supabase OAuth 2.1 with PKCE + consent page
See mcp_server/README.md for setup and tool reference.
We provide here a guide on how to set up locally your Draft'n run backend application.
You will need to install the following packages:
You will need to create two env files :
.envfile in theworkersfolder (you can copy the.env.examplefile)credentials.envat the root of the repository (you can copy thecredentials.env.examplefile)
In the credentials.env file (copied from credentials.env.example).
Generate the secret keys for:
BACKEND_SECRET_KEY
uv run python -c "import secrets; print(secrets.token_hex(32))+"FERNET_KEY
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"To use the backend of the app, you will need to run a Docker Compose file that launches the following services:
- postgres
- redis
- qdrant
- prometheus
- seaweedFS
By default, the credentials to run those services on Docker Compose are the same as in the credentials.env.example and .env.example files, except
for the seaweedFS service.
For the seaweedFS service, you need to go to the config/seaweedfs folder and create a s3_config.json file
based on the same model as the s3_config.json.example file.
The credentials that you will put will give you access to the s3 service of seaweedFS.
Here are the env variables with the default values that work for Docker Compose:
-
In
workers/.env:# Redis configuration REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=redis_password REDIS_QUEUE_NAME=ada_ingestion_queue # Worker configuration MAX_CONCURRENT_INGESTIONS=2 # Webhook queue configuration REDIS_WEBHOOK_QUEUE_NAME=ada_webhook_queue REDIS_WEBHOOK_DEDUP_TTL=86400 MAX_CONCURRENT_WEBHOOKS=2
-
In
credentials.env:# For Ingestion, set INGESTION_DB_URL (required): INGESTION_DB_URL=postgresql://postgres:ada_password@localhost:5432/ada_ingestion # FOR INGESTION QUEUE REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=redis_password REDIS_QUEUE_NAME=ada_ingestion_queue # FOR WEBHOOK QUEUE REDIS_WEBHOOK_QUEUE_NAME=ada_webhook_queue REDIS_WEBHOOK_DEDUP_TTL=86400 MAX_CONCURRENT_WEBHOOKS=2 # DB for the backend ADA_DB_URL=postgresql://postgres:ada_password@localhost:5432/ada_backend # QDRANT QDRANT_CLUSTER_URL=http://localhost:6333 QDRANT_API_KEY=secret_api_key # SeaweedFS # S3 CREDENTIALS FOR INGESTION S3_ENDPOINT_URL=http://localhost:8333 S3_ACCESS_KEY_ID=your_s3_access_key_id S3_SECRET_ACCESS_KEY=your_s3_secret_access_key S3_BUCKET_NAME=s3-backend S3_REGION_NAME=us-east-1
You will need to put in the
S3_ACCESS_KEY_IDand theS3_SECRET_ACCESS_KEYthe same values as in thes3_config.jsonfile you created earlier. TheS3_BUCKET_NAMEis the name of the bucket created by the docker compose file, which iss3-backendby default. If you need to use seaweedfs on another machine, you can change the S3_ENDPOINT_URL accordingly. If you need to run the s3 service with amazon s3 or another s3-like service, you need to change those 5 variables. Be careful, when using amazon s3, put theS3_ENDPOINT_URLto None, meaning:
S3_ENDPOINT_URL=By default, boto3 will use the amazon s3 endpoint.
In general, if you want to modify the credentials for any of those services, update the Docker Compose file accordingly.
To launch the services, navigate to the services folder and run:
docker compose up -dTo stop the services:
docker compose down -vTo clean the Docker volume if you suspect corruption:
docker volume pruneUpdate these if using remote services:
# QDRANT
QDRANT_CLUSTER_URL=xxxxx
QDRANT_API_KEY=xxxxx
# Supabase
SUPABASE_PROJECT_URL=http://localhost:54321
SUPABASE_PROJECT_KEY=xxxxx
SUPABASE_SERVICE_ROLE_SECRET_KEY=xxxx
SUPABASE_USERNAME=xxx@xxx.com
SUPABASE_PASSWORD=xxx
# Redis
REDIS_HOST=xxxx
REDIS_PORT=6379
REDIS_PASSWORD=xxxx
REDIS_QUEUE_NAME=ada_ingestion_queue
REDIS_WEBHOOK_QUEUE_NAME=ada_webhook_queue
REDIS_WEBHOOK_DEDUP_TTL=86400
MAX_CONCURRENT_WEBHOOKS=2
# Ingestion
INGESTION_DB_URL=postgresql://postgres:ada_password@localhost:5432/ada_ingestion
# Backend DB
ADA_DB_URL=postgresql://postgres:ada_password@localhost:5432/ada_backend
# URL to run the backend app
ADA_URL=http://localhost:8000Supabase is the service needed to link the FrontEnd and Backend.
To understand how to set up Supabase on local version, please check the FrontEnd repository.
If you want to use the local version,
put in the credentials.env file the following variable:
OFFLINE_MODE=True
OFFLINE_DEFAULT_ROLE="admin"If the following section, you will find instructions on how to set up Supabase information for the backend, both for local and cloud version.
How to set up Supabase env variables for the project (local and cloud version)
Define the Supabase project URL in the credentials.env file:
SUPABASE_PROJECT_URL=http://localhost:54321 (or your cloud project url)Here are also two important variables that you need to get from your Supabase project settings:
- anon key:
ey_... - service_role key:
eyJ...
You need to use them to fill those environment variables:
SUPABASE_PROJECT_KEY=*anon-key*
SUPABASE_SERVICE_ROLE_SECRET_KEY=*service_role key*Please check the Supabase documentation if you run the cloud version or the FrontEnd repository if you use the local version to know where to find those keys.
Finally you can create an user name and password on supabase and fill those variables (See again FrontEnd repistory instructions or Supabase cloud documentation):
SUPABASE_USERNAME=xxx
SUPABASE_PASSWORD=xxxAt the end of this part, you should have filled those variables in your credentials.env:
SUPABASE_PROJECT_URL=http://localhost:54321 (or your cloud project url)
SUPABASE_PROJECT_KEY=*anon-key*
SUPABASE_SERVICE_ROLE_SECRET_KEY=*service_role key*
SUPABASE_USERNAME=xxx
SUPABASE_PASSWORD=xxxFinally, generate with this script the INGESTION_API_KEY and WEBHOOK_API_KEY and their hashed versions, and put them in the credentials.env file:
# Generate ingestion API key (for ingestion endpoints)
uv run python -c "from ada_backend.services.api_key_service import _generate_api_key, _hash_key; key = _generate_api_key(); print('INGESTION_API_KEY =', key); print('INGESTION_API_KEY_HASHED =', _hash_key(key))"
# Generate webhook API key (for webhook internal endpoints)
uv run python -c "from ada_backend.services.api_key_service import _generate_api_key, _hash_key; key = _generate_api_key(); print('WEBHOOK_API_KEY =', key); print('WEBHOOK_API_KEY_HASHED =', _hash_key(key))"
# Generate scheduler API key (for scheduler internal endpoints)
uv run python -c "from ada_backend.services.api_key_service import _generate_api_key, _hash_key; key = _generate_api_key(); print('SCHEDULER_API_KEY =', key); print('SCHEDULER_API_KEY_HASHED =', _hash_key(key))"INGESTION_API_KEY=xxxx
INGESTION_API_KEY_HASHED=xxxx
WEBHOOK_API_KEY=xxxx
WEBHOOK_API_KEY_HASHED=xxxx
SCHEDULER_API_KEY=xxxx
SCHEDULER_API_KEY_HASHED=xxxxYou can configure Draft'n run to use your own custom Large Language Model (LLM) service by adding the following variables to your credentials.env file:
You can configure Draft'n run to use your own custom Large Language Model (LLM) service by copying the custom_models_example.json on a custom_models.json file in the root directory.
Configuration options:
-
completion_models: List of available completion modelsmodel_name: The name of your modelfunction_calling: Whether the model supports function callingmultimodal: Whether the model supports multimodal inputs (images, etc.)image_format: the type of image to put in the url for the payload to the llm ("jpeg", "png", etc.)constrained_completion_with_pydantic: Whether the model supports Pydantic-constrained outputsconstrained_completion_with_json_schema: Whether the model supports JSON schema-constrained outputs
-
embedding_models: List of available embedding modelsmodel_name: The name of your embedding modelembedding_size: The dimension of the embeddings
-
base_url: The API endpoint URL for your LLM provider -
api_key: Your API key for the provider
Note: If you dont have embedding models pass an empty list.
Usage:
Once configured, your custom models will appear in the model selection dropdowns throughout the application. You can reference them using the format provider_name:model_name (e.g., your_provider_name:your-completion-model).
Note: The custom_models.json file is automatically loaded by the application. No additional environment variables are required.
When you have done all configuration for local models you need to run seed again
make db-seedYou can also configure the parameters of the ingestion on the credentials.env file:
PAGE_RESOLUTION_ZOOM=1.0
NUMBER_OF_IMAGES_TO_DETERMINE_TYPE_OF_DOCUMENT=2
ENFORCE_PAGE_BY_PAGE_INGESTION=FalseHere is a breakdown of the variables:
PAGE_RESOLUTION_ZOOM: This parameter controls the zoom level for page resolution during ingestion. A value of 1.0 means no zoom, while values greater than 1.0 will increase the resolution.
NUMBER_OF_IMAGES_TO_DETERMINE_TYPE_OF_DOCUMENT: This parameter specifies how many images are used to determine the type of document during ingestion. A value of 2 means that the first two images will be analyzed to classify the document type. You set up to -1 to use all the images of the documents.
ENFORCE_PAGE_BY_PAGE_INGESTION: This parameter, when set to True, enforces page-by-page ingestion of documents. This is useful for ensuring that each page is processed individually, which can be important if you are using local llms with a short context window.
How it works:
If you set these variables, your custom model will appear as an option in the model selection dropdown after reseeding. When this model is selected, the backend will route requests to your custom LLM service instead of sending them to OpenAI or other providers.
Be careful. For now, when you have a local config, the first vision model and embedding model in your configuration will be used by default
for the ingestion
Nango is used as a headless OAuth token vault to handle OAuth integrations (Slack, HubSpot, Gmail, Google Calendar, and Neverdrop-branded variants). Generate encryption keys:
# Encryption key for tokens
openssl rand -base64 32
# Secret key for Nango API
openssl rand -hex 32Add to credentials.env:
NANGO_ENCRYPTION_KEY=your-generated-encryption-key
NANGO_SECRET_KEY=your-generated-secret-key
NANGO_DASHBOARD_PASSWORD=your-secure-passwordFor more details: See Nango documentation
Draft'n Run provider config keys currently include google-mail and google-calendar for standard Google flows, plus google-mail-neverdrop and google-calendar-neverdrop for Neverdrop-branded Google OAuth flows.
The HubSpot MCP component uses the hubspot OAuth provider. Its crm_upsert_contact_by_email tool returns the HubSpot contact id, the operation (created or updated), and a remote_url to the contact record when the connected portal can be resolved. Portal metadata is cached per HubSpot client; if lookup fails after a successful contact write, remote_url is returned as an empty string.
To enable Google login, set these in your credentials.env:
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secretHow to get them:
- Go to Google Cloud Console.
- Create OAuth 2.0 credentials (type: Web application).
- Add your appβs redirect URI (e.g. https://yourdomain.com/auth/google/callback).
- Copy the Client ID and Client Secret and paste them above.
Make sure the redirect URI matches what you configure in Google and your app. Do not commit these secrets to version control.
To comply with RGPD requirements, you need to set up automatic deletion of trace data older than 90 days.
- Install the
pg_cronextension in your PostgreSQL database:
\c postgres
CREATE EXTENSION IF NOT EXISTS pg_cron;- Create the scheduled cleanup job:
\c postgres
SELECT cron.schedule_in_database(
'cleanup-old-spans',
'0 2 * * *',
$q$
DELETE FROM traces.span_messages
WHERE span_id IN (
SELECT s.span_id FROM traces.spans s
WHERE s.start_time < NOW() - INTERVAL '90 days'
);
$q$,
'ada_backend'
)
WHERE NOT EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'cleanup-old-spans');This will automatically delete trace data older than 90 days every day at 2 AM.
Ensure Postgres is running.
Then:
make db-upgrade
make db-seed
make trace-db-upgradeRun the backend:
make run-draftnrun-agents-backendDefine in the credentials.env your username password:
ADMIN_USERNAME=your-admin-username
ADMIN_PASSWORD=your-admin-passwordRelaunch the backend run and then go to the following url:
ADA_URL/docs (documentation swagger)
ADA_URL/admin (admin console)
If you run locally, ADA_URL in your credentials.env should be
ADA_URL=http://localhost:8000
Ensure redis credentials are correctly set in both .env files.
Run the worker:
uv run python -m workers.worker.mainRun this command in a terminal:
uv run python -m ada_backend.run_schedulerDraft'n run includes a comprehensive observability stack for monitoring, tracing, and performance analysis. The stack uses industry-standard open-source tools that provide production-ready monitoring capabilities.
Our observability stack consists of:
- π₯ Prometheus - Metrics collection and storage for performance monitoring
- π Grafana - Visualization dashboards with real-time charts and alerts
- π Tempo - Distributed tracing for request flow analysis
- π FastAPI Metrics - HTTP performance metrics (latency, throughput, errors)
- π€ Agent Metrics - User-facing feature metrics for AI agent usage
- π Load Testing - Locust-based performance testing with immediate dashboard feedback
This setup provides both operational monitoring (for DevOps) and user-facing analytics (for business insights).
Control the observability stack with a single environment variable:
ENABLE_OBSERVABILITY_STACK=true # Enable (requires observability stack running)
ENABLE_OBSERVABILITY_STACK=false # Disable (backend runs standalone)When disabled, the backend runs without external observability dependencies. User-facing analytics still work normally.
If you only need the observability stack (without databases), run:
cd services
docker compose up -d prometheus tempo grafanaPerfect for development and testing:
cd services
docker compose up -d prometheus tempo grafanaAccess points:
- Grafana Dashboard: http://localhost:3000
- Prometheus Metrics: http://localhost:9090
- FastAPI Metrics: http://localhost:8000/metrics
Deploy observability stack on the same server as your backend:
Security Setup (Important!):
# In credentials.env - CHANGE THESE VALUES!
GRAFANA_ADMIN_USER=your-username
GRAFANA_ADMIN_PASSWORD=your-secure-password
# Keep localhost URLs for same-machine deployment
TEMPO_ENDPOINT=http://localhost:4318/v1/traces
PROMETHEUS_URL=http://localhost:9090
GRAFANA_URL=http://localhost:3000cd services
ln -s ../credentials.env .env
docker compose up -d prometheus tempo grafanaservices/.env β credentials.env is needed to correctly setup values from credentials.env for variable interpolation in docker-compose.yml.
π Security Note: Grafana is now secured with login authentication. Set strong credentials in credentials.env.
Deploy observability on dedicated servers/cluster:
# In credentials.env - Point to your monitoring infrastructure
TEMPO_ENDPOINT=https://tempo.your-domain.com/v1/traces
PROMETHEUS_URL=https://prometheus.your-domain.com
GRAFANA_URL=https://grafana.your-domain.com
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=secure-passwordNo code changes needed - just update environment variables!
-
Health Check: Run the observability test script
./scripts/test_observability_stack.sh
-
Load Testing: Generate metrics with realistic traffic
uv run python -m scripts.load_testing --users 10 --duration 60
-
Dashboard Access:
- Login to Grafana at your configured URL
- View "FastAPI Performance Dashboard"
- Monitor real-time metrics during load tests
Use the observability health check script to verify all components are connected and working:
./scripts/test_observability_stack.shThis script automatically tests:
- β FastAPI: Backend connectivity and metrics generation
- β Prometheus: Server health and target scraping status
- β Tempo: Tracing backend and trace collection
- β Grafana: Dashboard health and API connectivity
- β Metrics Integration: End-to-end data flow verification
For production environments, set environment variables to test remote infrastructure:
PROMETHEUS_HOST=prometheus.your-domain.com \
GRAFANA_HOST=grafana.your-domain.com \
./scripts/test_observability_stack.shWe use Pythonβs logging module with logging-config.yaml and logger.py.
At the app entry point:
from logger import setup_logging
setup_logging()import logging
LOGGER = logging.getLogger(__name__)
def some_function():
LOGGER.info("Info message from some_function")For more details, see the tracing documentation.
The backend includes a cron system for scheduled jobs (APScheduler + DB-backed). You can manage jobs via the /crons endpoints and extend with custom entrypoints. See ada_backend/services/cron/Readme.md for details.
AI models are the primary agents that you can run
- AI Agent: Agent that handle conversation with tools access capacity
- RAG : Agent that retrieves information from documents to answer
- LLM Call: Templated LLM Call
- Database Query Agent: Agent able to interrogate a SQL database
The start block is at the begining of each flow. It allows the user to determine what information the AI agent can use during the flow.
Tools are available for the AI Agent. Note that the AI Agent can also have other AI models as tools
- API call: A generic API tool that can make HTTP requests to any API endpoint.
- Internet Search with OpenAI: Answer a question using web search.
- SQL tool: Builds SQL queries from natural language
- RunSQLquery tool: Builds and executes SQL queries
This project uses the 'python-docx-template' library, licensed under the GNU Lesser General Public License v2.1. See https://github.com/elapouya/python-docx-template for details.