Skip to content
Technical preview. This site is published for review. Everything on it, including the API, tokens and module protocol, is subject to change.
Child workflows

Child workflows

A workflow can start other workflows. The workflow context w offers two ways: w.start_workflow for a detached child that outlives its parent, and w.execute_workflow for an awaitable child whose lifetime is bound to the parent. Both run as workflows in their own right, with their own key and their own history.

Detached: w.start_workflow

w.start_workflow(flow, args=None, kwargs=None)

Starts a fire-and-forget child and returns its workflow key as a string. The child’s main receives the positional arguments in args and the keyword arguments in kwargs, plus its own w first. The parent does not learn the child’s result, and the child keeps running when the parent ends.

CHILD = """
def main(w, name):
    print("Hi, ", name)
"""

def main(w):
    key = w.start_workflow(CHILD, kwargs = {"name": "John"})
    print(key)

Awaitable: w.execute_workflow

w.execute_workflow(flow, args=None, kwargs=None)

Starts a child whose lifetime is bound to this workflow and returns a (future, workflow_key) tuple. The future resolves to the value the child’s main returns; gather it like any other future, see Futures and gather. If the parent terminates while the child is still running, the child is canceled.

CHILD = """
def main(w, age, name):
    return "Hi, {}! You're {}!".format(name, age)
"""

def main(w):
    f, key = w.execute_workflow(CHILD, args = [99], kwargs = {"name": "John"})
    print(gather(f))

An error in the child surfaces when the parent gathers the future, exactly as an action error does.

What flow can be

The flow argument is one of:

  • A string or bytes holding Starlark source that defines main, as in the examples above.
  • A top-level function of the calling workflow’s definition. The child then runs that same definition, with that function as its entry point.

The design document mentions only the string and bytes forms; both work. The function form keeps everything in one file and lets a parent fan out over a list:

load("module:gitlab", "call_api")

def deploy_one(w, project_id, environment, token):
    status, _, _, err = gather(call_api(
        "POST", "/api/v4/projects/%d/deployments" % project_id,
        headers = {
            "Authorization": "Bearer " + token,
            "Content-Type": "application/json",
        },
        body = json.encode({"environment": environment}),
    ))
    if err != None or status != 201:
        fail("%s: status %s, error %s" % (environment, status, err))
    return environment

def main(w, project_id, environments, token):
    futures = [
        w.execute_workflow(deploy_one, kwargs = {
            "project_id": project_id, "environment": e, "token": token,
        })[0]
        for e in environments
    ]
    return gather(futures)

The function must be a top-level def of the workflow definition, reachable under its own name. A lambda, a nested function or a function imported with load() is rejected. The child re-executes the file’s top level, so the same load() statements apply.

What crosses the boundary

  • Arguments must be representable as AutoFlow values: the same types an action accepts. Sensitive values pass through opaquely, so a token can be forwarded as above.
  • Channels cannot be passed to a child, at any nesting depth. Only the channel’s name would travel; the child would bind a channel of its own that nobody sends to, so the call fails instead of silently misbehaving. A child that needs outside input creates its own channel and hands it out itself.
  • Annotations are inherited. The key-value annotations the caller set on StartWorkflow are copied to every child, and to their children.
  • The namespace is inherited, and the child runs on the parent’s shard.
  • Tokens are not shared. The child has its own signing secret, so the parent’s workflow token does not read or cancel the child. A caller that needs to manage a child from outside must be told its key and cannot obtain a token for it through this API today.

Limits and caveats

  • A child counts as a workflow of its own for the script size and channel value limits.
  • A child inherits the parent’s deadline. Detached or awaitable, it must finish within the schedule-to-complete timeout the caller set on the root workflow, so a detached child is a way to run past the parent’s completion, not past its lifetime. Bound an awaitable child more tightly by gathering its future with a timeout.
  • Channel tokens a child hands out through a module, for example with post_value, do not work yet: the send succeeds and the value is dropped. Create the channel, and wait on it, in the workflow that will gather the answer.

Related

Last updated on