Maya Headless Integration
The Maya Headless integration enables offline batch Maya pipeline operations from Vibrante-Node workflows. Unlike the live Houdini Bridge, the Maya integration uses an action-list pattern — a chain of maya_action_* nodes accumulates a list of operation dictionaries, which the maya_headless executor node then dispatches to a single batch Maya session (maya -batch or mayapy).
This design is optimal for render submissions, automated exports, and scene processing pipelines where a live, interactive Maya connection is neither needed nor appropriate. The Vibrante-Node graph specifies what to do; Maya runs headlessly and executes all operations in one session, then exits.
maya_headless fires. This makes it ideal for overnight render farms and automation pipelines.
Architecture
The integration follows a two-phase model: a graph-build phase (action accumulation) and a dispatch phase (batch Maya execution).
Vibrante-Node Graph Batch Maya Session
─────────────────── ──────────────────
maya_action_open_scene ─┐ maya -batch / mayapy
maya_action_render ├─ actions ─► runner script
maya_action_export_fbx ┘ processes action list
│ sequentially in one session
▼
maya_headless ──────────────────────── subprocess.Popen
blocks until exit
non-zero exit → node error
Each maya_action_* node reads actions_in, appends one action dictionary to the list, and passes actions_out to the next node. The list accumulates as execution flows through the chain. The maya_headless executor at the end receives the complete list, generates a temporary Maya Python script, and runs it in a batch Maya process.
The Action-List Pattern
How it works
Each action is a plain Python dictionary with a "type" key that identifies the handler to call inside the Maya runner script, plus any parameters that handler needs:
# Action: open a Maya scene file
{
"type": "open_scene",
"scene_path": "/projects/shot_010/scenes/shot_010.ma"
}
# Action: render a frame range
{
"type": "render",
"camera": "renderCam",
"start_frame": 1001,
"end_frame": 1100,
"output_dir": "/renders/shot_010/",
"renderer": "arnold"
}
# Action: export selection as FBX
{
"type": "export_fbx",
"node": "|geo_grp",
"output_path": "/exports/shot_010_geo.fbx"
}
# Action: run arbitrary MEL or Python
{
"type": "run_script",
"language": "python",
"code": "import maya.cmds as cmds; cmds.polyCube()"
}
Actions are processed in list order — the first action that fails halts the runner and the process exits with a non-zero code, which Vibrante-Node treats as a node error.
The accumulation rule
Every action node must follow this exact pattern to avoid mutating upstream data or failing on a None input:
actions = list(inputs.get("actions_in") or [])
# build one action dict and append
actions.append({ "type": "...", ... })
return {"actions_out": actions, "exec_out": True}
list(...) creates a shallow copy so the node never mutates the original list. or [] handles the case where actions_in is None (the first node in the chain has no upstream connection).
Writing a Maya Action Node
from src.nodes.base import BaseNode
class Maya_Action_Open_Scene(BaseNode):
name = "maya_action_open_scene"
def __init__(self):
super().__init__()
# [AUTO-GENERATED-PORTS-START]
self.add_input("actions_in", "list")
self.add_input("scene_path", "string", widget_type="text")
self.add_output("actions_out", "list")
# [AUTO-GENERATED-PORTS-END]
async def execute(self, inputs):
actions = list(inputs.get("actions_in") or [])
scene_path = inputs.get("scene_path", "").strip()
if not scene_path:
self.log_error("scene_path is required.")
return {"actions_out": actions, "exec_out": True}
actions.append({
"type": "open_scene",
"scene_path": scene_path
})
return {"actions_out": actions, "exec_out": True}
def register_node():
return Maya_Action_Open_Scene
- node_id follows the
maya_action_*pattern (e.g.maya_action_render). - category must be
"Maya". - icon_path use
"icons/maya.svg". - use_exec is
true— always include exec_in / exec_out. - actions_in / actions_out are mandatory ports for every action node in the chain.
- The type key in each action dict must exactly match a handler registered in the Maya runner script.
Node Library
Maya nodes live under the Maya category in the Library panel. There are 25 nodes in total:
| Node ID | Description |
|---|---|
maya_action_open_scene | Open a .ma or .mb scene file |
maya_action_new_scene | Create a new empty scene |
maya_action_save_scene | Save the current scene (in-place or to a new path) |
maya_action_import_file | Import a file into the current scene |
maya_action_import_reference | Add a file reference to the current scene |
maya_action_update_reference | Update an existing reference to a new file path |
maya_action_remove_reference | Remove a file reference |
maya_action_render | Render a frame range using the scene's render settings |
maya_action_set_render_settings | Override render globals (camera, resolution, renderer, output path) |
maya_action_export_fbx | Export a node or selection to FBX |
maya_action_export_abc | Export an Alembic cache for a node or selection |
maya_action_export_obj | Export geometry as OBJ |
maya_action_export_usd | Export a USD stage from the scene |
maya_action_set_attr | Set a node attribute value (cmds.setAttr) |
maya_action_get_attr | Read a node attribute value into the actions list as metadata |
maya_action_run_mel | Execute an arbitrary MEL expression or script file |
maya_action_run_python | Execute an arbitrary Python string inside the batch session |
maya_action_select | Select nodes by name pattern or type |
maya_action_apply_shader | Assign a material to a mesh node |
maya_action_create_node | Create any Maya node type with optional attribute preset |
maya_action_delete_node | Delete nodes matching a name pattern |
maya_action_set_frame_range | Set the timeline start and end frame |
maya_action_set_fps | Change the scene frame rate |
maya_action_optimize_scene | Run Maya's Optimize Scene Size utility |
maya_headless | Executor — spawns batch Maya and dispatches the action list |
Complete Workflow Example
Render pipeline
This example opens a scene, applies render settings, and renders a frame range. Connect the exec chain (exec_out → exec_in) alongside the data chain (actions_out → actions_in):
# Built programmatically from the Scripting Console
scene.clear()
# Node 1: open scene
open_node = scene.add_node_by_name("maya_action_open_scene", (0, 0))
open_node.set_parameter("scene_path", "C:/projects/shot_010/scenes/shot_010.ma")
# Node 2: override render settings
settings_node = scene.add_node_by_name("maya_action_set_render_settings", (300, 0))
settings_node.set_parameter("camera", "renderCam")
settings_node.set_parameter("width", 1920)
settings_node.set_parameter("height", 1080)
settings_node.set_parameter("output_dir", "C:/renders/shot_010/")
# Node 3: render
render_node = scene.add_node_by_name("maya_action_render", (600, 0))
render_node.set_parameter("start_frame", 1001)
render_node.set_parameter("end_frame", 1100)
# Node 4: save scene after render
save_node = scene.add_node_by_name("maya_action_save_scene", (900, 0))
# Node 5: executor
exec_node = scene.add_node_by_name("maya_headless", (1200, 0))
# Wire data chain (action list)
scene.connect_nodes(open_node, "actions_out", settings_node, "actions_in")
scene.connect_nodes(settings_node, "actions_out", render_node, "actions_in")
scene.connect_nodes(render_node, "actions_out", save_node, "actions_in")
scene.connect_nodes(save_node, "actions_out", exec_node, "actions_in")
# Wire exec chain (execution order)
scene.connect_nodes(open_node, "exec_out", settings_node, "exec_in")
scene.connect_nodes(settings_node, "exec_out", render_node, "exec_in")
scene.connect_nodes(render_node, "exec_out", save_node, "exec_in")
scene.connect_nodes(save_node, "exec_out", exec_node, "exec_in")
app.execute_pipeline()
Batch FBX export over a shot list
Combine Vibrante-Node's general data nodes with Maya action nodes to loop over a list of scene paths and export each one:
# Pseudocode — in a real workflow use a "for_each" or "list_iterate" node
# to drive this pattern from a CSV or asset database query.
shot_list = [
"/projects/shot_010/scenes/shot_010.ma",
"/projects/shot_020/scenes/shot_020.ma",
"/projects/shot_030/scenes/shot_030.ma",
]
for scene_path in shot_list:
actions = []
actions.append({"type": "open_scene", "scene_path": scene_path})
actions.append({"type": "export_fbx", "node": "|geo_grp",
"output_path": scene_path.replace(".ma", "_geo.fbx")})
actions.append({"type": "export_abc", "node": "|anim_grp",
"start_frame": 1001, "end_frame": 1100,
"output_path": scene_path.replace(".ma", "_anim.abc")})
# dispatch to maya_headless for each shot (or accumulate across shots)
Headless scene validation
# Validate scene health without opening Maya interactively.
# Uses maya_action_run_python to query the scene and write results to a temp file.
actions = []
actions.append({"type": "open_scene", "scene_path": "/projects/shot_010.ma"})
actions.append({
"type": "run_python",
"code": (
"import maya.cmds as cmds, json, pathlib\n"
"report = {\n"
" 'polycount': cmds.polyEvaluate(f=True),\n"
" 'cameras': cmds.ls(type='camera'),\n"
" 'errors': cmds.ls(type='unknown'),\n"
"}\n"
"pathlib.Path('/tmp/validation.json').write_text(json.dumps(report))"
)
})
# maya_headless runs this; downstream nodes read /tmp/validation.json
The maya_headless Executor Node
maya_headless is the only node in the Maya category that actually spawns a process. All other nodes are pure data transformers.
Ports
| Port | Direction | Type | Description |
|---|---|---|---|
actions_in | Input | list | The fully accumulated action list from the chain |
maya_exe | Input | string | Path to maya or mayapy executable. Defaults to auto-detect from MAYA_LOCATION or PATH. |
env_script | Input | string | Optional path to a shell script or Python file to source before launching Maya (e.g. for module setup) |
timeout | Input | float | Max seconds to wait for Maya to complete. 0 = no timeout. |
success | Output | bool | True if Maya exited with code 0 |
exit_code | Output | int | Raw process exit code |
log_output | Output | string | Captured stdout + stderr from the Maya process |
exec_out | Output | any | Fires when the process exits (regardless of exit code) |
Execution behaviour
- The executor writes the action list to a temporary JSON file, then passes the file path as an argument to the generated Maya Python runner script.
- The call to
subprocess.Popenblocks until Maya exits. For long renders, keep the Vibrante-Node window open and check the log panel for progress messages. - A non-zero exit code is logged as an error and
successis set toFalse. Theexec_outwire still fires so downstream error-handling nodes can respond. - The temporary runner script and action JSON are cleaned up after the process exits.
maya_headless blocks the Vibrante-Node execution engine for its entire duration. Do not use it for interactive workflows where the UI must remain responsive during Maya's run. For fire-and-forget submissions, consider wrapping the executor in a background thread node or submit to Deadline instead (see the Deadline integration).
Production Use Cases
- Overnight batch renders — chain open_scene → set_render_settings → render → save; loop over a shot list. Queue many such pipelines and let them run unattended.
- Automated FBX/Alembic export — open each asset scene, select the export group, export to a standard output path with consistent settings across a full character library.
- Scene validation — open, query poly count/unknown nodes/missing textures, write a JSON report, close — no artist involvement.
- Multi-shot look-dev — open each episode's shot, apply a shader preset via MEL, re-export. Consistent look across 30 shots in minutes.
- Reference management — iterate over a shot list, update broken reference paths to point to a new asset location, save. Replaces a manual per-file operation.
- USD pipeline ingestion — open a rigged Maya scene, bake animation, export USD per-shot, stage for downstream Houdini/Karma processing.
Tips & Conventions
Always open before operating
The batch Maya session starts empty. Every action chain must begin with maya_action_open_scene or maya_action_new_scene. Attempting to export or render without a scene loaded produces an error from the runner script.
One Maya session per executor
All actions in a single chain share one batch Maya process. If you need to operate on multiple independent scenes, either chain open → work → open (Maya supports re-opening), or build separate chains feeding separate maya_headless nodes and run them in parallel using Vibrante-Node's branching exec flow.
Action type matching
The "type" string in each action dict must exactly match a handler function name in the Maya runner script. If you add a custom handler to the runner script, your action node's type value must use the same string. There is no runtime discovery — mismatches produce a clear KeyError in the batch Maya log, which is captured in the log_output port.
Error handling in action chains
If you need actions to continue after a partial failure (e.g. skip a bad shot and continue to the next), add an "on_error": "continue" key to individual action dicts and handle it in the runner script. By default the runner halts on the first exception.
# Action with explicit continue-on-error flag
actions.append({
"type": "export_fbx",
"node": "|geo_grp",
"output_path": "/exports/shot_010_geo.fbx",
"on_error": "continue" # runner skips this action if export fails
})
Accessing environment variables in the runner
Custom variables set via Vibrante-Node's Settings → Environment Variables are propagated to the maya_headless subprocess environment. In the runner script, read them with os.environ.get("MY_VAR") as usual.