Project Succession
A node-based automation platform for VFX and game pipelines.
Overview
Project Succession is a cross-platform pipeline automation tool. You build workflows as node graphs, wire triggers to actions on a visual canvas, and the engine runs them across your DCC tools, files, and services. It ships as a desktop app (Windows, macOS, Linux) with a headless CLI and Docker runner for CI.
The frontend is a React / TypeScript editor built on React Flow; the execution engine is a Rust backend running each graph as a directed acyclic graph of triggers and actions, with async I/O throughout.
My role: Sole author; I designed and built the whole system: the node editor, the Rust execution engine, the DCC integrations, and the packaging/release pipeline.
Gallery
Features
Build pipelines visually
- Node-based editor. Drag triggers and actions onto a canvas and wire execution and data flow
- 270+ built-in node types, with a parameter panel supporting enums, file pickers, colors, and inline expressions
DCC & engine integrations
- Deep two-way integrations with Blender, Maya, Houdini, Substance Painter/Designer, Photoshop, Unreal, Unity, Godot and more.
Extensibility
- Custom Python nodes. Its own
pythonAPI for node generation..pyfiles can be loaded from any OS location and behave like native nodes. - Subgraphs, For Loops, and a Parallel Dispatcher for concurrent batch work with a configurable parallelism cap.
Scale & control
- Run multiple independent graphs in parallel, each with fully isolated state
- Path-access policies and audit logging for safe use in a studio or enterprise
- AI-ready: an MCP server lets tools like Claude read the node palette and build graphs
Deep Dive
A graph is a DAG. Triggers sit at the roots and fire on events; execution flows downstream along the wired edges, and each node hands its outputs to the children connected to it. The interesting part is data routing: a node can emit several named outputs, and each child should receive exactly the payload wired into its input handle, not everything the parent produced.
When a parent fires, the engine looks at which of the parent’s output handles are wired to the child, picks the matching payload, and falls back to the full event only if nothing specific is wired:
// succession_app/src/lib.rs — routes a parent's event data to the correct child input
fn select_parent_event_input(
event_data: &serde_json::Value,
parent_outputs: &HashMap<String, Vec<(String, String)>>,
node_id: &str,
) -> Option<serde_json::Value> {
// Which of the parent's output handles are wired to this child?
let wired_handles: Vec<&String> = parent_outputs
.iter()
.filter(|(_, children)| children.iter().any(|(child_id, _)| child_id == node_id))
.map(|(output_handle, _)| output_handle)
.collect();
// Prefer the payload on a wired handle; otherwise pass the whole event through.
if let Some(data_for_input) = wired_handles
.iter()
.find_map(|handle| event_data.get(handle.as_str()))
{
return Some(data_for_input.clone());
}
Some(event_data.clone())
}
Graphs are acting on json with a given structure on input0, input1 etc. and output0, output1 etc.
A graph resolves into a data structure like
{
"identifier": "The key of this data block",
"type": "a string value defining the type of this data, e.g. object, or string",
"value": "the actual data held by this data block"
}