Skip to main content
Jeremias Meister - Tools & Pipeline
Project Succession

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.

Building and running a pipeline end to end in the node editor.
A graph resolving; triggers fire and data flows downstream through the wired nodes.

Features

Build pipelines visually

DCC & engine integrations

Extensibility

Scale & control

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"
}