Script Runner IO
Hera provides the Input and Output Pydantic classes to help combat sprawling function declarations of inputs and
outputs when using script annotations for runner scripts. It has the added bonus of letting you return values by
referencing their name, instead of setting outputs by position in a Tuple.
It lets you go from a multiple-argument function declaration and a tuple return to a single input and output. Compare
the equivalent workflows below:
@script(constructor="runner")
def my_function(
param_int: int = 42,
an_object: Annotated[MyObject, Parameter(name="obj-input")] = MyObject(
a_dict={"my-key": "a-value"}, a_str="hello world!"
),
artifact_int: Annotated[int, Artifact(name="artifact-input", loader=ArtifactLoader.json)],
) -> Tuple[
Annotated[int, Parameter(name="param-int-output")],
Annotated[int, Parameter(name="another-param-int-output")],
Annotated[str, Parameter(name="a-str-param-output")],
]:
print(param_int)
print(artifact_int)
...
return 42, -1, "Hello, world!" # Hope I didn't mix these up!
from hera.workflows.io import Input, Output
class MyInput(Input):
param_int: int = 42
an_object: Annotated[MyObject, Parameter(name="obj-input")] = MyObject(
a_dict={"my-key": "a-value"}, a_str="hello world!"
)
artifact_int: Annotated[int, Artifact(name="artifact-input", loader=ArtifactLoader.json)]
class MyOutput(Output):
param_int_output: Annotated[int, Parameter(name="param-int-output")],
another_param_int_output: Annotated[int, Parameter(name="another-param-int-output")],
a_str_param_output: Annotated[str, Parameter(name="a-str-param-output")],
@script(constructor="runner")
def my_function(my_input: MyInput) -> MyOutput:
print(my_input.param_int)
print(my_input.artifact_int)
...
return MyOutput(
param_int_output=42,
another_param_int_output=-1,
a_str_param_output="Hello, world!",
)
Pydantic V1 or V2?
Importing Input and Output from the hera.workflows.io submodule automatically matches the version of your Pydantic
installation.
If you need to use V1 models when you have V2 installed, you should import Input and Output from the
hera.workflows.io.v1 (or hera.workflows.io.v2) module explicitly. The V2 models will not be available if you
have installed pydantic<2, but the V1 models are usable for either version, allowing you to migrate at your own pace.
Script Inputs Using Input
For script inputs, you should create a subclass class of Input, and declare all your input parameters (and artifacts)
as fields of the class. You can use Annotated to declare Artifacts add metadata to your Parameters.
from typing import Annotated
from pydantic import BaseModel
from hera.workflows import Artifact, ArtifactLoader, Parameter, script
from hera.workflows.io import Input
class MyObject(BaseModel):
"""This is a model class, to be used within an Input."""
a_dict: dict
a_str: str = "a default string"
class MyInput(Input):
"""This is the Input class."""
param_int: int = 42
an_object: Annotated[MyObject, Parameter(name="obj-input")] = MyObject(
a_dict={"my-key": "a-value"}, a_str="hello world!"
)
artifact_int: Annotated[int, Artifact(name="artifact-input", loader=ArtifactLoader.json)]
@script(constructor="runner")
def pydantic_io(
my_input: MyInput,
) -> ...:
...
This will create a script template named pydantic_io, with input parameters "param_int" and "obj-input", but not
"my_input"; the template will also have the "artifact-input" artifact. The YAML generated from the Python will look
like the following:
templates:
- name: pydantic-io
inputs:
parameters:
- name: param-input
default: '42'
- name: obj-input
default: '{"a_dict": {"my-key": "a-value"}, "a_str": "hello world!"}'
artifacts:
- name: artifact-input
path: /tmp/hera-inputs/artifacts/artifact-input
script:
...
Script Outputs Using Output
The Output class works in the same way as Input, but comes with two extra special variables, exit_code and
result. The exit_code is used to exit the container when running on Argo with the specific exit code - it is set to
0 by default. The result is used to print any serializable object to stdout, allowing you to use the result output
parameter between steps or tasks.
If you want an output parameters/artifacts with the name
exit_codeorresult, you can declare another field with an annotation of that name, e.g.my_exit_code: Annotated[int, Parameter(name="exit_code")].
Aside from the exit_code and result, the Output behaves exactly like the Input:
from typing import Annotated
from hera.workflows import Artifact, Parameter, script
from hera.workflows.io import Output
class MyOutput(Output):
param_int: Annotated[int, Parameter(name="param-output")]
artifact_int: Annotated[int, Artifact(name="artifact-output")]
@script(constructor="runner")
def pydantic_io() -> MyOutput:
return MyOutput(exit_code=1, result="Test!", param_int=42, artifact_int=my_input.param_int)
See the full Pydantic IO example here!