Skip to content

Custom Procedures#

When built-in nodes don't meet your needs, you can create custom procedure nodes.

[!WARNING] > Client Implementation Required

Defining a procedure in Python creates only the interface and serialization. For execution on device, you must also implement the corresponding logic in the iOS client (CaptureGraphEngine/Models-Procedures/ProcedureNodes).

Without the iOS implementation, the app will fail to load your procedure.

The @make_procedure Decorator#

All procedures are dataclasses. The @make_procedure decorator transforms a class into a frozen, keyword-only dataclass compatible with the runtime:

import capturegraph.procedures as cgp

@cgp.make_procedure
class MyCustomProcedure(cgp.Procedure[cgp.PString]):
    """A custom procedure that returns a string."""
    prefix: str
    suffix: str

Defining Inputs#

Static Settings#

Use standard Python types for configuration:

@cgp.make_procedure
class RepeatString(cgp.Procedure[cgp.PString]):
    text: str       # Static string
    times: int      # Static integer
    enabled: bool   # Static boolean

Procedure Inputs#

Use Procedure[T] for data flow connections:

@cgp.make_procedure
class ProcessImage(cgp.Procedure[cgp.PImage]):
    input_image: cgp.Procedure[cgp.PImage]    # Connected procedure
    brightness: float                          # Static setting

Return Types#

Specify the return type in Procedure[T]:

class MyNumber(cgp.Procedure[cgp.PNumber]): ...
class MyAction(cgp.Procedure[cgp.PVoid]): ...
class MyImage(cgp.Procedure[cgp.PImage]): ...

Validation#

Add custom validation in __post_init__:

@cgp.make_procedure
class RepeatString(cgp.Procedure[cgp.PString]):
    text: str
    times: int

    def __post_init__(self) -> None:
        super().__post_init__()  # Always call super first

        if self.times < 1:
            raise ValueError("times must be at least 1")

Generic Procedures#

For nodes that pass data through (like IfThenElse), propagate the type from inputs.

Generic Type Variable#

@cgp.make_procedure
class PassThrough[T: cgp.PType](cgp.Procedure[T]):
    input_value: cgp.Procedure[T]

@forward_types Decorator#

Automatically set the return type based on input fields using @forward_types:

import capturegraph.procedures as cgp

@cgp.make_procedure
@cgp.forward_types(cgp.PType, "input_value")
class PassThrough[T: cgp.PType](cgp.Procedure[T]):
    input_value: cgp.Procedure[T]

Now PassThrough(input_value=some_image_procedure) becomes a Procedure[PImage].

Best Practices#

  1. Immutability: Procedures are frozen. Don't mutate after initialization.

  2. Type Safety: Use specific PType subclasses (PImage, PLocation) rather than generic PType.

  3. Documentation: Add docstrings to help others understand your node.

  4. Composition First: Before creating a new node, check if you can compose existing nodes:

# Often better than a custom node
def my_workflow():
    return cgp.ProcedureSequence(
        procedures=[existing_node_a(), existing_node_b()]
    )
  1. Naming: Use descriptive names that indicate the return type:
  2. CaptureX — Captures and returns X
  3. ProcessX — Transforms X
  4. UserInputX — Gets X from user

See Also#