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#
-
Immutability: Procedures are frozen. Don't mutate after initialization.
-
Type Safety: Use specific
PTypesubclasses (PImage,PLocation) rather than genericPType. -
Documentation: Add docstrings to help others understand your node.
-
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()]
)
- Naming: Use descriptive names that indicate the return type:
CaptureX— Captures and returns XProcessX— Transforms XUserInputX— Gets X from user
See Also#
- Types and Nodes — Understanding PTypes and node categories
- Procedure API — Full
Procedurebase class reference - Types API — All available PTypes