Genroc supports child processes, which you can imagine as function calls.
The parent specifies the child's name and its input. The child instance
is then started and the parent instance is parked (its status is still running,
but it is effectively waiting for the child to finish). When the child
finishes, its output becomes the task's result.
You can use a child process to reuse a piece of logic in multiple processes, or as a wrapper for an external system, which allows independent versioning.
child action
Let's define two processes:
# greet.genroc.yaml
nameUnique process identifier.: greetUnique process identifier.
input_schemaJSON Schema used to validate the input payload when starting a new instance.:
typeThe value's JSON type, or a list of types it may take.: objectThe value's JSON type, or a list of types it may take.
propertiesThe named members of an object, each a schema.:
who: {typeThe value's JSON type, or a list of types it may take.: stringThe value's JSON type, or a list of types it may take.}
requiredWhich properties must be present. A property not listed here may be absent, and reading it yields null.: [who]
tasksOrdered list of execution tasks. Control advances linearly unless a switch case redirects.:
- idTask identifier, unique within the definition.: welcomeTask identifier, unique within the definition.
outputtasks.welcome.output — string: "Hello, ${inputinput → object{who}.whoinput.who → string}"
switchRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.: endRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.
outputoutput — object{message}:
messagemessage — string: "$: outputsoutputs → object{welcome}.welcomeoutputs.welcome → string"
# parent.genroc.yaml
nameUnique process identifier.: parent_processUnique process identifier.
tasksOrdered list of execution tasks. Control advances linearly unless a switch case redirects.:
- idTask identifier, unique within the definition.: spawn_childTask identifier, unique within the definition.
actionDescribes the action to perform. Omit for switch-only (routing) tasks.:
typeSingle child call: runs one named process and waits. The result is its output, unwrapped.: childSingle child call: runs one named process and waits. The result is its output, unwrapped.
nameName of the child process to invoke.: greetName of the child process to invoke.
inputtasks.spawn_child.action.input — object{who}:
whowho — string: world
result_schemaJSON Schema validating and exposing the child's output.:
typeThe value's JSON type, or a list of types it may take.: objectThe value's JSON type, or a list of types it may take.
propertiesThe named members of an object, each a schema.:
message: {typeThe value's JSON type, or a list of types it may take.: stringThe value's JSON type, or a list of types it may take.}
outputself.result → object{message?}: "$: selfself → object{result}.resultself.result → object{message?}"
switchRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.: endRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.
outputoutputs.spawn_child.message → string|null: "$: outputsoutputs → object{spawn_child}.spawn_childoutputs.spawn_child → object{message?}.messageoutputs.spawn_child.message → string|null"
The child action is quite similar to the fetch action, as it also
has an input and a result_schema. Additionally, it needs the name of the
child process.
Let's now apply the processes and run the parent.
> genctl apply
latest: greet - -> v1 (new)
latest: parent_process - -> v1 (new)
> genctl run parent_process
started: 3kjfx4vm parent_process@v1 (running)
You can now print logs:
> genctl logs @last
TIME LEVEL ID EVENT TASK
--- 2026-09-16 +02:00 ---
14:07:46 INFO 3kjfx4vm inst_created by=no-auth:anonymous
14:07:46 INFO 3kjfx4vm child_spawned spawn_child msg="1 children"
14:07:46 INFO 02pcayvt inst_created input={"who":"world"}
14:07:46 INFO 02pcayvt inst_completed welcome output={"message":"Hello, world"}
14:07:46 INFO 3kjfx4vm child_collected spawn_child
14:07:46 INFO 3kjfx4vm inst_completed spawn_child output="Hello, world"
This automatically shows the logs of the parent and all its children, so we can see exactly what happened.
We can also list instances like this:
> genctl instances
ID STATUS PROCESS UPDATED CREATED CODE ERROR
3kjfx4vm completed parent_process@v1 3m ago 3m ago
However, this shows only root instances (the instances that have no parent);
we have to add the --children parameter to see all of them.
> genctl instances --children
ID STATUS PROCESS PARENT UPDATED CREATED CODE ERROR
3kjfx4vm completed parent_process@v1 - 3m ago 3m ago
02pcayvt completed greet@v1 3kjfx4vm 3m ago 3m ago
child_map action
It runs multiple children in parallel,
and they can be of different kinds. You specify each child independently in
the children object.
nameUnique process identifier.: checkoutUnique process identifier.
input_schemaJSON Schema used to validate the input payload when starting a new instance.:
typeThe value's JSON type, or a list of types it may take.: objectThe value's JSON type, or a list of types it may take.
propertiesThe named members of an object, each a schema.:
order_id: { typeThe value's JSON type, or a list of types it may take.: stringThe value's JSON type, or a list of types it may take. }
requiredWhich properties must be present. A property not listed here may be absent, and reading it yields null.: [order_id]
tasksOrdered list of execution tasks. Control advances linearly unless a switch case redirects.:
- idTask identifier, unique within the definition.: prepareTask identifier, unique within the definition.
actionDescribes the action to perform. Omit for switch-only (routing) tasks.:
typeKeyed child call: named processes run concurrently. The result is keyed by child name.: child_mapKeyed child call: named processes run concurrently. The result is keyed by child name.
childrenKeyed map of child processes to run concurrently. Keys become the access names in outputs.taskID.:
stock:
nameName of the child process to invoke.: reserve-stockName of the child process to invoke.
inputtasks.prepare.action.children.stock.input — object{order_id}: { order_idorder_id — string: "${inputinput → object{order_id}.order_idinput.order_id → string}" }
result_schemaJSON Schema validating and exposing this child's output.: { typeThe value's JSON type, or a list of types it may take.: booleanThe value's JSON type, or a list of types it may take. }
payment:
nameName of the child process to invoke.: charge-cardName of the child process to invoke.
inputtasks.prepare.action.children.payment.input — object{order_id}: { order_idorder_id — string: "${inputinput → object{order_id}.order_idinput.order_id → string}" }
result_schemaJSON Schema validating and exposing this child's output.: { typeThe value's JSON type, or a list of types it may take.: numberThe value's JSON type, or a list of types it may take. }
outputself.result → object{payment, stock}: "$: selfself → object{result}.resultself.result → object{payment, stock}"
switchRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.: endRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.
outputoutput — object{charged, reserved}:
reservedreserved — boolean: "$: outputsoutputs → object{prepare}.prepareoutputs.prepare → object{payment, stock}.stockoutputs.prepare.stock → boolean"
chargedcharged — number: "$: outputsoutputs → object{prepare}.prepareoutputs.prepare → object{payment, stock}.paymentoutputs.prepare.payment → number"
The parent instance waits for all the children to finish.
The children's outputs are collected into an object mirroring the children keys.
child_list action
This one runs a dynamic number of children in parallel; all of them are of the same kind.
The fields are the same as on a single child, but instead of input there is over,
which must be an array of inputs.
nameUnique process identifier.: greet_allUnique process identifier.
input_schemaJSON Schema used to validate the input payload when starting a new instance.:
typeThe value's JSON type, or a list of types it may take.: arrayThe value's JSON type, or a list of types it may take.
itemsThe schema every element of an array conforms to.: { typeThe value's JSON type, or a list of types it may take.: stringThe value's JSON type, or a list of types it may take. }
maxItemsMost allowed array elements.: 10Most allowed array elements.
tasksOrdered list of execution tasks. Control advances linearly unless a switch case redirects.:
- idTask identifier, unique within the definition.: prepareTask identifier, unique within the definition.
actionDescribes the action to perform. Omit for switch-only (routing) tasks.:
typeList fan-out: one child per element of 'over'. The result is an array in 'over' order.: child_listList fan-out: one child per element of 'over'. The result is an array in 'over' order.
nameName of the child process to invoke for every element.: greetName of the child process to invoke for every element.
# mapping into Array<object{who}>
overmap(input, (name) => { who: name }) → array<object{who}>: "$: mapmap(input, (name) => { who: name }) → array<object{who}>(inputinput → array<string>, (namename → string) => { whomap(input, (name) => { who: name }) → array<object{who}>: namename → string })"
result_schemaJSON Schema validating and exposing EACH child's output; the result is an array.:
typeThe value's JSON type, or a list of types it may take.: objectThe value's JSON type, or a list of types it may take.
propertiesThe named members of an object, each a schema.:
message: { typeThe value's JSON type, or a list of types it may take.: stringThe value's JSON type, or a list of types it may take. }
requiredWhich properties must be present. A property not listed here may be absent, and reading it yields null.: [message]
outputself.result → array<object{message}>: "$: selfself → object{result}.resultself.result → array<object{message}>" # result: Array<object{message}>
switchRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.: endRequired. Routing: a shorthand ("next", "end", "$task-id") or an ordered list of cases.
outputoutput — object{result}:
resultresult — array<object{message}>: "$: outputsoutputs → object{prepare}.prepareoutputs.prepare → array<object{message}>"
Notice we are using the map function to create a properly shaped array.
The result_schema applies to each individual item, and the results are collected into an array
in the same order as the inputs.
Action fields
The three child actions share most of their fields:
| Field | Used by | Meaning |
|---|---|---|
name | all | The process to start. Required. |
version | all | The version to run; the latest when omitted. |
input | child, child_map entries | The child's input. |
over | child_list | An array expression; one child is started per item, with the item as its input. Required. |
children | child_map | Named entries, each with the fields above. Required. |
input_schema | all | Declares the input sent to the child. |
result_schema | all | Declares the child's output, as the parent reads it. |
raises | all | The raise codes the parent can catch, with their data schemas (more). |
On child_map, each entry under children has its own name, version, input, schemas and raises.
Parent/child type checking
Genroc checks the parent/child compatibility when the definitions are applied.
It checks that the inputs you pass fit the child's input_schema, and that the child's output and error data fit the parent's result_schema and raises.
The lsp doesn't check the compatibility between processes locally,
run genctl apply --check-only to check on the server.
Child error handling
Genroc doesn't use classic "catching" for child processes. The main reason is that we require everything to be explicitly typed, and processes should be self-contained in terms of their interface.
So every process needs to explicitly specify which errors it can produce. This is also the reason why errors don't bubble up through the tree: if they did, a process would leak types from its children, which we don't allow.
Raised errors are understood as a branching tool for expected failures.
# child (fetch action)
on_error:
- code: ["http.404"]
raise:
code: "item_not_found"
message: "Item ${input.item_id} not found"
# parent (child action)
raises:
item_not_found: null # we need to specify explicitly what errors we expect
on_error:
- code: ["item_not_found"]
goto: $handle_item_not_found
Read more about
panicandraisein the error handling section.
The raises field
Specifies which errors we expect. It's an object whose keys are error codes and whose values are (optional) JSON schemas.
If you define an error schema, you can then access error.data (like with fetch error responses).
Importing child process
You can import a child process to avoid duplicating its fields in the YAML.
# parent.genroc.yaml
name: parent_process
tasks:
- id: spawn_child
action:
type: child
<<: "$process: ./child.genroc.yaml"
input:
who: world
output: "$: self.result"
switch: end
output: "$: outputs.spawn_child.message"
$process: is a resolver: it finds the specified genroc file,
builds its types and returns name, input_schema, result_schema and raises.
<< operator
The << operator is the YAML convention for spread syntax.
It is used to prefill an object with default values. Genroc expands the resolver into the object. The equivalent pure YAML would look like this:
- id: spawn_child
action:
type: child
<<:
# product of `$process: ./child.genroc.yaml`
name: greet
result_schema:
type: object
properties:
message: {type: string}
input:
who: world