PyOutline Tutorial
This tutorial walks you through building increasingly complex OpenCue jobs with PyOutline and pycuerun.
Note: PyOutline is the Python library for defining jobs; pycuerun is the CLI tool for launching them. This tutorial covers both — you’ll use PyOutline’s API to build jobs and pycuerun to submit them from the command line. See also the PyCuerun Tutorial.
What You’ll Learn
- Creating basic single-layer jobs
- Building multi-layer render pipelines
- Configuring dependencies between layers
- Writing custom layer classes
- Using sessions for data exchange
- Pre/post processing with child layers
- Debugging jobs with local execution
Prerequisites
- OpenCue environment running with at least one RQD host
- PyCue and PyOutline installed (
pip install opencue-pycue opencue-pyoutline) CUEBOT_HOSTSenvironment variable set
Tutorial 1: Your First Job
Create tutorial_01.py:
import outline
import outline.modules.shell
# Create the outline (job definition)
ol = outline.Outline("tutorial-01", show="testing", shot="test")
# Create a shell layer that runs over 10 frames
layer = outline.modules.shell.Shell(
"hello",
command=["echo", "Hello from frame #IFRAME#"],
range="1-10"
)
# Add the layer to the outline
ol.add_layer(layer)
# Launch the job
outline.cuerun.launch(ol, use_pycuerun=False, os="Linux")
print("Job submitted!")
Run it:
python tutorial_01.py
This creates a job with 10 frames, each executing echo Hello from frame N.
Tutorial 2: Multi-Layer Pipeline
Create tutorial_02.py:
import outline
import outline.modules.shell
ol = outline.Outline("tutorial-02-pipeline", show="testing", shot="test")
# Layer 1: Render
render = outline.modules.shell.Shell(
"render",
command=["echo", "Rendering frame #IFRAME#"],
range="1-20"
)
ol.add_layer(render)
# Layer 2: Composite (depends on render)
composite = outline.modules.shell.Shell(
"composite",
command=["echo", "Compositing frame #IFRAME#"],
range="1-20"
)
composite.depend_on(render) # Frame-by-frame dependency
ol.add_layer(composite)
# Layer 3: Publish (depends on all composite frames)
publish = outline.modules.shell.Shell(
"publish",
command=["echo", "Publishing frame #IFRAME#"],
range="1-20"
)
publish.depend_all(composite) # Layer-on-layer dependency
ol.add_layer(publish)
outline.cuerun.launch(ol, use_pycuerun=False)
This creates a three-stage pipeline:
render: All frames can run in parallelcomposite: Each frame waits for the corresponding render framepublish: No frame runs until all composite frames finish
Tutorial 3: Using Chunking
Create tutorial_03.py:
import outline
import outline.modules.shell
ol = outline.Outline("tutorial-03-chunking", show="testing", shot="test")
# Process 100 frames in chunks of 10 (creates 10 tasks)
layer = outline.modules.shell.Shell(
"batch-process",
command=["echo", "Processing frame #IFRAME#"],
range="1-100"
)
layer.set_chunk_size(10)
ol.add_layer(layer)
outline.cuerun.launch(ol, use_pycuerun=False)
print("Submitted 100 frames in 10 chunks of 10")
Chunking is useful when your application has significant startup overhead. Each task processes multiple frames, reducing total overhead.
Tutorial 4: Custom Layer Classes
Create tutorial_04.py:
import os
import outline
from outline import Layer
class FileCounter(Layer):
"""A custom layer that counts files in a directory."""
def __init__(self, name, **args):
Layer.__init__(self, name, **args)
self.require_arg("directory")
def _setup(self):
"""Validate the directory exists during setup."""
directory = self.get_arg("directory")
if not os.path.isdir(directory):
raise outline.LayerException(f"Directory not found: {directory}")
def _execute(self, frames):
"""Execute for each frame/chunk."""
directory = self.get_arg("directory")
files = os.listdir(directory)
count = len(files)
print(f"Found {count} files in {directory}")
# Store the result for other layers to access
self.put_data("file_count", {"directory": directory, "count": count})
ol = outline.Outline("tutorial-04-custom", show="testing", shot="test")
counter = FileCounter(
"count-files",
directory="/tmp",
range="1"
)
ol.add_layer(counter)
outline.cuerun.launch(ol)
Custom layers override _execute() for the main logic and optionally _setup() for validation.
Tutorial 5: Data Exchange Between Layers
Create tutorial_05.py:
import json
import outline
from outline import Layer
class Producer(Layer):
"""Produces data for downstream layers."""
def _execute(self, frames):
for frame in frames:
result = {
"frame": frame,
"output": f"/renders/frame_{frame:04d}.exr",
"status": "complete"
}
self.put_data(f"frame_{frame}", result)
print(f"Produced data for frame {frame}")
class Consumer(Layer):
"""Consumes data from the producer layer."""
def _execute(self, frames):
producer = self.get_outline().get_layer("producer")
for frame in frames:
data = producer.get_data(f"frame_{frame}")
print(f"Consuming frame {frame}: {data['output']}")
ol = outline.Outline("tutorial-05-data", show="testing", shot="test")
producer = Producer("producer", range="1-5")
consumer = Consumer("consumer", range="1-5")
consumer.depend_on(producer)
ol.add_layer(producer)
ol.add_layer(consumer)
outline.cuerun.launch(ol)
Sessions automatically serialize data as YAML, so you can exchange complex Python objects between frames.
Tutorial 6: Pre- and Post-Processing
Create tutorial_06.py:
import outline
from outline import Layer, LayerPreProcess, LayerPostProcess, OutlinePostCommand
from outline.modules.shell import Shell
class SetupScene(LayerPreProcess):
"""Downloads and prepares scene files before rendering."""
def _execute(self, frames):
print("Setting up scene files...")
self.put_data("scene_ready", {"path": "/scenes/main.ma", "ready": True})
class VerifyOutput(LayerPostProcess):
"""Verifies render outputs after layer completion."""
def _execute(self, frames):
print("Verifying render outputs...")
class NotifyTeam(OutlinePostCommand):
"""Sends notification after the entire job completes."""
def _execute(self, frames):
print("Job complete! Notifying team...")
ol = outline.Outline("tutorial-06-hooks", show="testing", shot="test")
# Main render layer with pre/post processing
render = Shell("render", command=["echo", "Rendering #IFRAME#"], range="1-10")
render.add_child(SetupScene("setup-scene"))
render.add_child(VerifyOutput("verify"))
ol.add_layer(render)
# Post-job notification
ol.add_layer(NotifyTeam("notify"))
outline.cuerun.launch(ol, use_pycuerun=False)
Child layers run automatically:
SetupScene(PreProcess) runs before any render frame startsVerifyOutput(PostProcess) runs after all render frames completeNotifyTeam(PostCommand) runs after the entire job completes
Tutorial 7: Outline Scripts with pycuerun
Instead of Python scripts with explicit launch calls, you can write outline scripts that pycuerun loads.
Create tutorial_07.outline:
import outline.modules.shell
# Layers are automatically added to the current outline
render = outline.modules.shell.Shell(
"render",
command=["echo", "Rendering frame #IFRAME#"],
range="1-20"
)
composite = outline.modules.shell.Shell(
"composite",
command=["echo", "Compositing frame #IFRAME#"],
range="1-20"
)
composite.depend_on(render)
Launch with pycuerun:
# Basic launch
pycuerun tutorial_07.outline
# Override frame range
pycuerun -f 1-50 tutorial_07.outline
# Launch paused
pycuerun -p tutorial_07.outline
# Inspect without launching
pycuerun -i tutorial_07.outline
Tutorial 8: Local Debugging
Debug a failing frame by running it locally:
# Step 1: Inspect the outline
pycuerun -i tutorial_07.outline
# Step 2: Execute frame 5 of the render layer locally
pycuerun -e 5-render tutorial_07.outline
# Step 3: Run with debug logging
pycuerun -D -e 5-render tutorial_07.outline
For full local execution of all frames:
pycuerun --backend local tutorial_07.outline
The local backend runs frames sequentially on your machine, respecting dependencies.
Tutorial 9: Environment Variables and Configuration
Create tutorial_09.outline:
import os
import outline
import outline.modules.shell
# Get the current outline
ol = outline.current_outline()
# Set job-wide environment variables
ol.set_env("RENDER_QUALITY", "preview")
ol.set_env("OUTPUT_DIR", "/renders/preview")
# Create a layer with its own environment
render = outline.modules.shell.Shell(
"render",
command=["echo", "Quality: $RENDER_QUALITY, Engine: $RENDER_ENGINE"],
range="1-10"
)
render.set_env("RENDER_ENGINE", "arnold")
# Another layer inherits job env but not render's layer env
preview = outline.modules.shell.Shell(
"preview",
command=["echo", "Quality: $RENDER_QUALITY"],
range="1-10"
)
preview.depend_all(render)
Launch with additional environment variables:
pycuerun --env RENDER_QUALITY=production --env OUTPUT_DIR=/renders/final tutorial_09.outline
Tutorial 10: Simulation Dependencies
Create tutorial_10.py:
import outline
from outline.modules.shell import Shell
ol = outline.Outline("tutorial-10-simulation", show="testing", shot="test")
# Simulation layer where each frame depends on the previous frame
sim = Shell(
"fluid-sim",
command=["echo", "Simulating frame #IFRAME#"],
range="1-100"
)
# Frame N waits for frame N-1 (sequential execution)
sim.depend_previous(sim)
ol.add_layer(sim)
# Render can process frames as they become available
render = Shell(
"render-sim",
command=["echo", "Rendering sim frame #IFRAME#"],
range="1-100"
)
render.depend_on(sim) # Frame-by-frame
ol.add_layer(render)
outline.cuerun.launch(ol, use_pycuerun=False)
The depend_previous creates a chain: frame 1 runs first, then frame 2, then frame 3, etc. The render layer can start processing frames as soon as the corresponding simulation frame completes.
Summary
| Tutorial | Concept |
|---|---|
| 1 | Basic job creation and submission |
| 2 | Multi-layer pipelines with dependencies |
| 3 | Frame chunking for efficiency |
| 4 | Custom layer classes |
| 5 | Data exchange via sessions |
| 6 | Pre/post processing hooks |
| 7 | Outline scripts with pycuerun |
| 8 | Local debugging |
| 9 | Environment variables |
| 10 | Simulation dependencies |