Airflow
Postgres · scheduler · webserver
Self-hosted distributed runtime · v1.0 research
A distributed runtime for DAGs, services, and agentic workflows, shipped as a single JAR with zero external dependencies. The scheduler, the wire protocol, the failure handling, and the persistence layer are all built from scratch.
The problem
The second machine was in another country. Titan began as a control plane so the first could hand work to the second, nothing more.
Each step was the same problem one size larger, and something built to borrow memory had quietly become the infrastructure. That is also where it stopped being a script: workers die mid-job, masters restart, results go missing, someone has to say yes, each of those had to become a mechanism rather than a patch.
The constraint turned out not to be unusual. Teams have machines sitting idle and workloads that outgrow one box. Every tool that solves this properly assumes you already run infrastructure, the thing you were trying to avoid.
Postgres · scheduler · webserver
Cassandra / SQL · history service
Head node · worker fleet
One JAR
The idea · the cluster
The answer to that gap has to be a runtime with nothing to operate. Here is the shape of one.
Core execution and routing work with the store absent, you lose state recovery and the KV bus, not scheduling.
The idea · the answer
Three components is easy to draw; the hard part was writing them. One executable was the constraint, not the outcome:
Owning every layer underneath is what buys that, and the size is the receipt: the shipped JAR is 132 KB, and the pre-shade artifact is within a kilobyte of it because there is nothing to bundle in.
| Layer | Built from scratch |
|---|---|
| Scheduler | Dependency resolution, capability + affinity routing, event-driven dispatch |
| Transport | 33-opcode binary RPC over raw TCP, no gRPC, no Netty |
| Execution | Task runners behind one interface: scripts, services, detached processes, archives, file staging |
| Persistence | RESP-compatible store with AOF, KV, and replication |
| Concurrency | ~4,900 lines of framework-free Java across dispatch, heartbeat, and scale loops |
“Zero dependencies” means the engine: scheduler, protocol, store and execution ship in one JAR and pull in nothing. The optional layers are honest about their cost, Flask if you want the browser dashboard, PyYAML if you write pipelines in YAML, the mcp package if you connect an AI client. The SDK's own client is standard library only: sockets, structs, base64, zipfile.
Where it fits · the case for it
Before the mechanisms: whether any of this is relevant to you.
And if none of those describe you, reach for the mature tool. Airflow, Temporal and Ray are excellent, and this deck will not argue you out of a system that works.
The idea · the thesis
Zero dependencies only pays off if one engine can cover more than one job. That is the thesis.
A DAG is already the common execution representation across schedulers, workflow engines, and compute frameworks. Titan starts there, then extends the same object three times, because each world breaks a different assumption the base quietly makes.
A plain DAG assumes every node runs anywhere, finishes, and is known before you submit. Batch breaks the first. Services and agents break the other two.
Assumption held. But a node that can run anywhere is useless on mixed hardware.
Capability tags route strictly; affinity pins a child to its parent's node; a DelayQueue holds timed work at zero CPU; retry and a dead-letter queue stop one poison job stalling the graph.
Assumption broken. A DAG node reports done and exits. A server does neither.
Completion was split from process lifetime: the deploy returns DEPLOYED_SUCCESS and downstream unlocks while the process runs on under a supervisor. Teardown became explicit: stop_service suppresses the restart.
Assumption broken. You cannot submit a graph whose shape the next LLM call decides.
The graph became writable mid-run: stages submit further DAGs, agent_run_id stitches them into one run, TitanStore carries state across nodes, and a gate holds the run for a human up to 48 hours.
Same scheduler. Same protocol. Same dashboard. Most orchestrators pick one world and excel in it. The narrower, testable claim: these three are the same object, and the seams are where the engineering went.
How it works · 1 of 7 · what that lets you draw
“One representation, three extensions” is a claim. These seven slides are the mechanisms that make it true.
A job declares a list of parents and nothing else, satisfiedDeps ≥ declaredDeps, and no rule requires a submission to be connected. Every shape below is legal in a single submit_dag().
How it works · 2 of 7 · beyond a plain DAG
A plain dependency graph has no vocabulary for a human decision, a process that never exits, or a node that doesn't exist yet. Titan runs all three on the same scheduler, the same protocol, and the same dashboard as everything else.
agent_run_id.Two shapes are not legal and fail quietly rather than loudly: a cycle, and a parent id that was never submitted. Both leave the job parked in the waiting room forever. The Constructor runs a DFS cycle check before deploy; the YAML and SDK paths do not.
How it works · 3 of 7 · routing
A node is now ready and legal. The scheduler still has to decide which machine runs it.
GPU job bypasses idle GENERAL nodes until a capable one frees up. Tags are free-form strings the worker advertises at startup.affinity: true the Master looks up the exact physical node that ran the parent and pins the child there, hitting the local filesystem cache instead of the network.How it works · 4 of 7 · the wire
Every message, client↔Master and Master↔Worker, is an 8-byte header plus a UTF-8 payload over a raw TCP socket. No HTTP, no serialization framework.
The receiver compares the version byte against its own and rejects mismatched frames rather than guessing. There is no negotiation handshake.
| Off | Size | Field |
|---|---|---|
| 0 | 1 | version, currently 1 |
| 1 | 1 | opcode, 33 defined |
| 2 | 1 | flags, reserved |
| 3 | 1 | reserved |
| 4 | 4 | length, u32 big-endian |
| 8 | n | payload, UTF-8 |
How it works · 5 of 7 · failure
Handling partial failure is the part most from-scratch orchestrators skip. Eight mechanisms are wired through the scheduler core.
Master dials each worker; silence past threshold marks it dead and re-queues its in-flight jobs.
Up to 3 retries, 4 attempts total, then quarantine, not an infinite loop.
Poison pills are isolated with logs intact; children are cancelled so the DAG doesn't stall.
Port conflicts and rejected deploys skip the retry loop, deterministic errors don't burn attempts.
recoverState() rescues in-flight work on restart so a DAG resumes instead of stranding.
Process trees are forcibly terminated on expiry; 30s RPC read timeouts surface stuck peers.
Saturated cluster re-queues rather than dropping or overloading a node.
Completion reports retry 5× at 1→2→4→8s so a network blip can't lose a finished result.
How it works · 6 of 7 · elasticity
A reconciliation loop compares pressure against capacity every 15 s; a heartbeat loop dials every worker every 10 s. Between them the cluster grows, heals and shrinks unattended.
All GENERAL workers saturated and under the ceiling, so the Master submits a job to its own scheduler, DEPLOY_PAYLOAD|Worker.jar, priority 10. A worker launches the JVM, which self-registers.
The GPU node stops answering heartbeats, is marked DEAD and dropped, and its in-flight jobs re-queue onto healthy nodes. If it returns, its own 30-second loop rejoins it.
A reclaim candidate must be ephemeral, not the root, at zero load, hosting no service, idle over 45 s. Highest port first, one per cycle, four guards before anything you care about.
How it works · 7 of 7 · the human
Set one field (hitl_message) and the SDK injects a gate job after that step and rewires every downstream dependent onto it. The engine never learns what HITL is; it sees a normal DAG with an extra node.
The pipeline then pauses durably: 48-hour default, survives a closed laptop, resumes on an Approve in the dashboard. Reject cancels downstream instead of leaving it hanging.
Airflow needs custom sensors for this. Prefect needs custom state handlers. Here it's a keyword argument.
Gate exits 0 · downstream releases
Gate fails · downstream cancelled
Quality checks before an expensive training run. Compliance sign-off before a deploy. Editorial review before a report publishes.
Ingress
That is the whole engine. The rest is putting work into it and watching it run, starting with the five front doors.
Declarative, version-controlled pipelines. Commit to git, re-run any time.
Shape decided at runtime, agent loops, conditional branching, dynamic fan-out.
Deploy a YAML pipeline from the terminal, or drive the cluster from the Java shell: stats, job status, logs, cancel with cascade, approve or reject a gate, deploy a service, decommission a worker.
Drag nodes, draw edges, deploy in one click. Generates the equivalent YAML and SDK code.
11 tools over stdio. Describe the pipeline in English from Claude Desktop or Cursor; the agent writes the scripts and submits.
All five land in the same dispatch loop and the same live visualizer, per-job logs, status, workspace files, HITL banners. The front door does not change the graph.
Start here
Five front doors, one graph. Here is the cluster they all submit to, from cold.
$ ./titan-dev.sh up [INFO] Booting Titan Development Environment... [OK] Engine JAR found. [SETUP] Verifying Python SDK... [OK] Python SDK ready. [START] Starting TitanStore (Port 6379)... [START] Starting Titan Master (Port 9090)... [START] Starting General Worker (Port 8080)... [START] Starting UI Dashboard (Port 5000)... ==================================================== TITAN CLUSTER IS LIVE ==================================================== [+] Master Node: localhost:9090 (PID: 5952) [+] Worker Node: localhost:8080 (PID: 5991) [+] TitanStore: localhost:6379 (PID: 5944) [UI] Dashboard: http://localhost:5000
Ctrl+C tears the whole cluster down and frees every port.No Postgres. No broker. No container runtime. No cluster to join. The next command submits a real DAG:python titan_sdk/titan_cli.py deploy agent.yaml
The run, live
Node colour is live state, and every node carries the worker port that ran it, so routing decisions are visible, not inferred. The amber node here is a HITL gate holding the pipeline for a human. Dashed edges lead to work that hasn't been unblocked yet.
Same view regardless of how the DAG arrived, YAML, SDK, Constructor, or MCP. Click a node for its live log stream; the files panel below serves every artifact the run produced. Known limit: live status here is resolved through the client-side DAG manifest, which is being reworked, the graph, the logs and the file panel are reliable, but live status can lag or misreport. The Timeline and Cluster pages read spans and the store directly, so use them as the authority when the two disagree.
Observability · the scheduler
The DAG pages describe your pipelines. This one describes the scheduler running them. A pipeline can be perfectly healthy while the control plane is the problem, and none of that shows up as a red node: it shows up as work not moving, with no stated cause. So the page is built to state the cause. Three questions, and what answers each.
Example 01 · declarative
That is the view. Here are five real pipelines that produce it, from the simplest upward.
name: "titan-comprehensive-test" project: true # zip the surrounding folder jobs: - id: "step-1-init" file: "scripts/init.py" priority: 10 # jumps the queue - id: "step-2-fetch" file: "src/processor.py" delay: 2 # ClockWatcher holds it 2s depends_on: ["step-1-init"] - id: "step-3-process" file: "src/processor.py" affinity: true # same node as its parent depends_on: ["step-2-fetch"] - id: "step-4-server" type: "service" # stays alive, auto-restarts file: "src/server.py" port: 9000 depends_on: ["step-3-process"]
fetch, for local cache reuse.One declarative file exercises the delay queue, the priority queue, affinity routing, and the service supervisor.
Example 02 · agentic
while iteration < max_iterations:
iteration += 1
# N researchers, N decided by the planner
researcher_jobs = [
TitanJob(
job_id = f"research-worker-{tag}-{i}",
filename = _WORKER,
args = f"{run_id} {iteration} {i} ...",
)
for i in range(len(subtopics))
]
client.submit_dag(f"ITER{iteration}",
researcher_jobs,
agent_run_id=run_id)
wait_for_signals(client, researcher_keys)
# THE AGENTIC DECISION
decision = json.loads(
client.store_get(f"...:eval:{iteration}"))
if decision["decision"] == "SYNTHESIZE":
break # LLM says done
else:
subtopics = decision["gaps"] # go deeper
[AGENT] Planner decided 4 subtopics: [0] Performance, Scalability, Latency [1] Cost and Operational Overhead [2] Retrieval Quality, Embeddings [3] Consistency, Freshness, Integration [AGENT] Iteration 1/3, 4 subtopic(s) [AGENT] researcher-1-0, done [AGENT] researcher-1-3, done ← out of order: [AGENT] researcher-1-1, done truly parallel [AGENT] researcher-1-2, done [AGENT] evaluator-1, done [AGENT] Evaluator: research complete.
Example 03 · no code
Drag nodes onto the canvas, drag from a node's port to draw a dependency, and set script, capability, priority, and gate message in the sidebar. A DFS cycle check blocks deploy if you wire a loop. Deploy submits straight to the running cluster, and the button becomes a link into the live visualizer.
The output panel emits the equivalent YAML and Python SDK for whatever is on the canvas, so the pipeline from Example 01 can be built by dragging, then copied back out as a committed file. The visual path is an on-ramp, not a walled garden.
Applications
Four representative pipelines. So what is it actually for?
Chunk a corpus in parallel, embed on GPU workers, fan in to build the index. Vector store and embedding endpoint stay up as services.
Re-indexing is a pipeline you re-run, not a script someone babysits.
batch + service + gateN configs train concurrently on GPU nodes, each writing its metric to TitanStore. A fan-in job picks the winner.
Wall-clock is the slowest single run, not the sum.
batchA planner sets the subtopics, N researchers run concurrently, an evaluator loops or stops, a synthesizer writes up.
Agents span machines, so the parallelism is real and the output outlives the session.
agentA model server runs as a permanent service with port management and auto-restart; batch jobs call it from the same cluster.
One substrate for the always-on thing and the work that calls it.
serviceThe pattern underneath: work that needs real compute, real time, or a real decision in the middle, and a team that would rather own the substrate than operate a platform to get it.
Shapes Titan runs today at research and medium scale, a lab or a small team, tens of workers, machines you already own. Throughput at production load is not yet measured; see What v1 is not.
Where it fits · the surface
Not a roadmap, what is implemented and running today. The full inventory is in the docs.
Observability is not an add-on: one span per dispatch attempt, queue wait attributed per job, critical path computed, and the dispatch loop split five ways — in the box, no Prometheus, no Grafana.
Validation
Benchmarks written for your own system prove very little. So we ran someone else's.
WfCommons publishes DAGs recorded from production scientific workflows: astronomy, genomics, seismology, agriculture. They ship the task graph, the dependencies and the measured runtimes. Titan replays them, so correctness is a checkable fact rather than a claim we make about ourselves.
Five published instances, the largest a 9,805-task Montage with a single level 6,285 tasks wide.
all completedEvery parent-child edge checked against the recorded spans: did the child really start after its parent finished.
0 ordering violations2 to 11 levels deep, 28 to 6,285 wide. A scheduler can be right on one topology and wrong on another.
5 topologiesOrdering is a property of the scheduler, not of the machine it ran on, so this result transfers to any deployment. Performance numbers do not: those are single-host and stated as such.
Porting took about twenty lines. WfFormat describes a DAG as tasks with parent references; so does Titan. The convergence was not planned, it is what the problem shape forces.
Landscape
The honest version, where Titan wins, and where it plainly does not.
Categories and capabilities, not raw quality. Airflow, Ray, Nomad and Temporal are mature systems with thousands of contributors. Some rows are lost on purpose.
This is not a replacement claim. Titan does not replace Kubernetes, Airflow, Ray, Nomad or Temporal, and nothing in this deck argues that it should. Kubernetes orchestrates containers; Titan schedules a job graph and manages processes, which is a different layer, and Titan runs perfectly happily inside a cluster Kubernetes is already managing. It also works the other way round: Titan can be the infrastructure, or it can be just the execution layer underneath something else. The Dagster integration in this repo has Dagster owning the asset graph, scheduling and lineage while Titan runs the work, via a TitanEngine resource of about thirty lines. The table below is here so you can see which parts overlap and which do not, and decide whether the overlap is relevant to you.
| Capability | Titan | Airflow | Ray | Nomad | Temporal |
|---|---|---|---|---|---|
| Dynamic DAG mutation at runtime | Native | Limited | Yes | No | Yes |
| Tasks spawn tasks (agentic) | Native | No | Yes | No | Possible |
| Services + batch in one runtime | Yes | No | Yes | Yes | Partial |
| Human-in-the-loop gates | Native | Custom sensors | No | No | Signals |
| Visual DAG builder | Native | No | No | No | No |
| Observability stack needed for scheduler internals | None, in the box | StatsD/OTel + Prometheus + Grafana | Dashboard built in | Metrics endpoint + Prometheus | Prometheus + Grafana |
| Dispatch loop broken down by phase | Five phases | One critical-section metric | No | Coarse eval/plan timings | No |
| External deps to run core | None | Postgres, webserver | Head node | Consul (opt) | Cassandra / SQL |
| Master HA / leader election | No (v2) | Yes | Yes | Yes | Yes |
| mTLS / auth | No (v2) | Yes | Yes | Yes | Yes |
| Production-ready | Research v1.0 | Yes | Yes | Yes | Yes |
Honest scope
The rows lost on the previous slide deserve more than a footnote.
| Constraint | Status |
|---|---|
| Master HA | SPOF, Raft leader election on v2 |
| Transport security | Unencrypted TCP, mTLS on v2 |
| Isolation | Process-level, Docker drivers on v2 |
| Recurring schedules | Delayed execution today, cron on roadmap |
| Network | Assumes a flat address space (LAN / VPC) |
| Service liveness | Probed once at deploy, never re-probed |
| Scale-up cost | ~2 s of dispatch-loop time per spawned worker |
| Measured scale | 9,805 tasks in one submission, 5 workers / 20 slots, one host |
The mechanisms above give durability and self-healing, no committed state or in-flight job is lost across a crash. What they do not give is availability during a Master outage.
The gap is availability-during-failure, not data loss. That distinction is the honest version of the claim, and it's the one worth making.
Run it inside a trusted VPC or over an SSH tunnel. Not multi-tenant, not internet-facing, not hard-SLA, by design, for now.
Status
A research runtime engineered from first principles, currently v1.0. It handles complex distributed execution loops and agentic workflows, and it will find edge cases in non-standard environments. Contributions to harden the core are welcome.
github.com/ramn51/
titan-orchestrator
ramn51.github.io/
titan-orchestrator
Apache 2.0 · Ram Narayanan A S
← → or space to navigate