Starlark quick reference
A workflow definition is a Starlark program. AutoFlow parses it with the dialect below, then calls its main function once per workflow.
The dialect
| Option | Enabled | Effect |
|---|---|---|
Set | yes | The set type and the set() built-in are available. |
While | yes | while loops are allowed. |
TopLevelControl | no | if, for and while are rejected at the top level of a file. |
GlobalReassign | no | A top-level name cannot be rebound after its first binding. |
Recursion | no | A function cannot call itself, directly or indirectly. Use a loop. |
Entry point
A workflow definition must define a top-level main. AutoFlow calls it as main(w, *args, **kwargs), with args and kwargs taken from the StartWorkflow request. The signature decides which arguments the definition accepts: passing one that main does not declare fails the workflow. The value main returns is the workflow result, and a main with no return yields None.
def main(w, project_id, environment):
print(w.workflow_key, project_id, environment)w is a frozen module with three members.
| Member | Signature | Semantics |
|---|---|---|
w.workflow_key | attribute, string | The opaque key of this workflow. |
w.start_workflow | w.start_workflow(flow, args = None, kwargs = None) | Starts a detached child and returns its workflow key. flow is Starlark source as string or bytes, or a top-level function of the calling workflow’s definition. |
w.execute_workflow | w.execute_workflow(flow, args = None, kwargs = None) | Starts an awaitable child and returns a (future, workflow_key) tuple. The child is canceled if the parent terminates. |
A channel cannot be passed to a child workflow.
Predeclared symbols
On top of the Starlark universe (print, fail, len, range and the rest), AutoFlow predeclares the following. load is a statement, allowed only at the top level of a file.
| Symbol | Signature | Semantics |
|---|---|---|
time | time.now(), time.parse_duration(), time.parse_time(), time.from_timestamp(), time.time(), time.is_valid_timezone(), and the time.nanosecond to time.hour constants | The starlark-go time library. time.now() returns workflow time read from the history, not the clock of the machine running the round. |
json | json.encode(), json.decode(), json.indent() | The starlark-go JSON library. |
math | math.ceil(), math.floor(), math.sqrt(), and the rest of the library | The starlark-go math library. |
struct | struct(**fields) | A record with a fixed set of named fields. + merges two structs, right side wins. Hashable when every field is. |
module | module(name, **members) | A named namespace value. Compared by identity, not hashable. |
channel | channel() | Creates a channel. Read it with gather, watch it with select, or hand it to an action so the module can send into it. ch.name is its identity. |
sleep | sleep(duration) | Durable sleep. Returns None. Positional only. |
timer | timer(duration) | Returns a future that resolves to None after the duration. Positional only. |
gather | gather(input, timeout = None, timeout_value = None) | Blocks until the input resolves. input is one future or channel, or a list of them; the return mirrors that shape. On timeout it returns timeout_value and consumes nothing. See Futures and gather. |
select | select(cases, timeout = None, timeout_value = None) | cases is a positional dict of name to future or channel. Returns the name of a ready case, or timeout_value. It reports readiness and does not consume: a channel value stays until a gather reads it. See Futures and gather. |
poll | poll(action, check, interval, timeout, args = None, kwargs = None, timeout_value = None) | Re-invokes action every interval until the CEL expression check over ret holds, then returns a future with the value, or timeout_value once timeout elapses. interval and timeout must be positive. A retryable module error counts as not ready. |
derived_action | derived_action(action, transform) | Returns a new action that runs transform where the action runs. transform must be a top-level def in the same file, must take the invoker as its first positional parameter, must call it exactly once, and must not capture names from an enclosing scope. |
load | load("module:[<path>/]<name>[?file=<f>]", "symbol", alias = "other_symbol") | Imports variables, actions and file symbols from a module. Only the module: scheme is accepted, file is the only query parameter, and a cycle in the module graph is an error. Imported variables are frozen. |
Durations are duration values: write 5 * time.second, or pass a string that Go’s ParseDuration accepts where a duration is expected. A plain int is rejected.
Value types
| Type | Where it comes from | Notes |
|---|---|---|
future | An action call, timer(), poll(), w.execute_workflow() | Resolves to a permanent value. Gathering it again returns the same value. See Futures and gather. |
channel | channel(), or a main() parameter the caller bound | Never closed, so there is no end of stream. Values arrive in the order AutoFlow received them. |
action | A load of a module, or derived_action() | Callable, passable, not hashable. Calling it returns a future. |
sensitive_string, sensitive_bytes | main() arguments, module variables, action results | Opaque. print and repr render a placeholder, in is refused, and there is no way back to string or bytes. + with a plain or sensitive operand yields a sensitive result. A workflow cannot construct one from ordinary data. |
struct | struct(), a module | Fixed fields, read with .. |
module | module(), a load | Named namespace. |
| proto message | An action result | Readable and passable. A workflow cannot construct one: the proto module is not predeclared. |
NoneType, bool, int, float, string, bytes, list, tuple, dict, set, time.time, time.duration | Standard Starlark | int must fit a signed 64-bit integer to leave the workflow. |
Determinism
A workflow definition is replayed against its history whenever the workflow resumes, so it must take the same decisions in the same order every time.
- No I/O. There is no filesystem, no network, no environment and no randomness. Every side effect goes through an action.
- No wall clock.
time.now()reads the recorded workflow time. - No exception handling. Starlark has no
try, so any error abortsmainand fails the workflow. An action error surfaces where its future is gathered, not where the action was called. - A future nobody gathers reports nothing, so a failing action that nobody waits for does not fail the workflow. An operation scheduled in the round that ends the workflow is dropped rather than run, which is why a workflow definition gathers a future even when it does not need the value.
- A call into a module that fails or times out is retried: autocore retries it with exponential backoff, by default 20 attempts over about an hour and three quarters, every attempt under the same idempotency key, and the workflow sees only the final outcome. An error the module returns as its answer is final and surfaces where the future is gathered.
poll()is a different mechanism: it re-invokes an action whose result does not yet satisfy its check. selectpicks deterministically among several ready cases, but which one it picks is unspecified. A timeout never wins over a case that is ready in the same round.- Top-level code in a module file must not invoke actions: during
loadan action resolves to a stub that refuses to be called. - Globals freeze once a file has loaded, so per-run state lives in locals and closures.
- Every interpreter thread has a computation-step budget, so a runaway workflow fails at the same instruction on every replay. See Limits.
More detail
- Predeclared symbols in the design doc: every symbol with examples and edge cases.
- The AutoFlow manual: execution model, child workflows, failure states.