genroc

docs / Guides / Process definition

Child processes

Learn how to split and reuse process logic.

Genroc supports child processes which you can imagine as a function calls. Parent needs to specify a child name and it’s input parameters. Child process is then started and the parent process is parked (status is still running, but the process is effectively waiting for the child to finish). When the child process finishes the child output is then mapped as a task result.

You can use child process to reuse a piece of logic in multiple processes. Or as a wrapper for some 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"

Child action is quite similar to the fetch action as it also have input and result_schema. Additionally it needs 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 logs of the parent and all his child processes, so we can see what exactly 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 will show only root instances (the instances which have no parent), we have to add --children parameter to see all.

> 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 allows you to run multiple children in parallel, they can be of a differend 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"

Parent process waits for all the children to finish. Each child output is mapped to an object mirroring the children keys.

child_list action

This one allows run dynamic number of processes in parallel, all of then need to be of a same kind. The proerties are the same as single child, but instead of input we have over, which must be 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 map function to create properly shaped array.

The result_schema applies to individual item and the results are mapped into array, in the same order as the inputs.

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 editor doesn’t check this; to check without applying, run genctl apply --check-only.

Child error handling

Genroc doesn’t use classic “catching” for child processses. Main reason that we require everything to be explicitly typed and processes should be self-contained in terms of the interface.

So every process needs to specify which errors it can produce explicitly. This is also reason why errors don’t bubble through the tree, if they would, then it would be leaking types from it’s children, which we don’t allow.

Raised errors are understood as a branching tools for expected failiures.

# 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 panic and raise in the error handling section

The raises field

Specifies what errors we expect. It’s an object where keys are error codes and values are json schemas (optional). If you define error schema you can then access error.data (like fetch error responses).

Importing child process

You can import child process to reduce duplicated 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 it’s types and returns name, input_schema, result_schema and raises.

<< operator

The << operator is yaml convention for spread syntax.

It is used to prefill object with default values. Genroc will expand the resolver into object. 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