NOUKAI

Executing Flows

Learn how to execute flows and customize behavior.

Flows are executed via execute(), which runs the entire flow to completion and returns an ExecuteResult (or a PausedResult when the flow pauses for tool calls).

Basic execution

Get a flow proxy and call execute() with a message. The recommended pattern is to set org and project defaults when constructing the client:

from noukai_sdk import Noukai
 
with Noukai(org="acme", project="spelling") as client:
    flow = client.flow("grade-3")
    result = flow.execute(message="The cat sat on the mat.")
    print(result.output)

message is the flow's first input. It is a shorthand for parameters={"message": "..."} — see below for details on how parameters compose.

You can also override the defaults with a fully-qualified flow identifier or per-call kwargs (see Per-call overrides at the end of this guide).

Execution options

execute() accepts the following keyword arguments:

result = flow.execute(
    message="Input text",                              # Shorthand initial input
    parameters={"essay_topic": "ocean"},               # Flat dict of extra initial inputs
    block_overrides={                                  # Per-step config overrides
        "essay_grader": {"rubric": "college_level"},
    },
    attachments=[                                      # Up to 10 HTTPS attachments
        {"url": "https://example.com/cat.jpg", "mime_type": "image/jpeg"},
    ],
    tools=[],                                          # OpenAI-style tool definitions (max 64)
    tool_choice="auto",                                # "auto" | "none" | "required" | {...}
    tool_handler=None,                                 # Optional auto-loop handler
    max_tool_rounds=10,                                # Safety bound on tool loop
    version="draft",                                   # "draft" | "production" | <int>
    trace=False,                                       # Capture input/output snapshots
    timeout=None,                                      # Per-request timeout (seconds)
)

message and parameters — the flow's initial inputs

A flow has a set of named initial inputs declared in its definition. message is just the conventional name for the first one. To pass additional initial inputs, use parameters as a flat dict mapping input name → value (not nested by block):

# Flow declares initial inputs: message, target_grade, locale
result = flow.execute(
    message="The cat sat on the mat.",
    parameters={
        "target_grade": "grade-3",
        "locale": "en-US",
    },
)

block_overrides — per-step config overrides

To override the config of an individual step (e.g. swap the prompt template, change the model on a single LLM block) use block_overrides, keyed by step ID:

result = flow.execute(
    message="Grade this essay",
    block_overrides={
        "essay_grader": {"rubric": "college_level"},
        "feedback_formatter": {"style": "verbose"},
    },
)

parameters is a flat map of flow-level inputs; block_overrides is keyed by step ID and overrides that step's config. They are not interchangeable.

version — which version of the flow to run

version accepts:

  • "draft" (default) — the in-progress draft
  • "production" — the currently promoted production version
  • An integer N — a specific published version number (e.g. version=3)

Strings other than "draft" and "production" (including "main") raise ValueError.

result = flow.execute(message="Test input", version="production")
result = flow.execute(message="Test input", version=3)

tools and tool_handler

tools accepts OpenAI-format tool definitions. With tool_handler set, the SDK loops automatically: the server pauses for tool calls, the handler runs, the SDK resumes — up to max_tool_rounds (default 10). Without tool_handler, the SDK returns a PausedResult for the caller to drive manually. See Tool calls for the full pattern.

trace

When True, the server captures full input/output snapshots in the trace. Timing, token counts and cost are recorded either way — trace adds the snapshots used to debug bad outputs. Retrieve the trace via flow.run(execution_id).trace() after execution; see Tracing & debugging.

Results

A successful execute() returns an ExecuteResult:

result = flow.execute(message="hi")
 
result.status           # "completed" | "failed"
result.output           # The flow's final output (alias for result.result)
result.result           # Same as .output
result.execution_id     # Unique execution ID — pass to flow.run(...) for trace
result.flow_id          # Flow ID this run was executed against
result.block_count      # Number of steps the flow contained
result.requires_tool_calls  # Always False on ExecuteResult

ExecuteResult does not embed the trace inline. To fetch timing/cost/tokens, look up the execution by ID:

result = flow.execute(message="hi", trace=True)
trace = flow.run(result.execution_id).trace()
total_cost = sum(float(s.cost_usd) for s in trace.steps if s.cost_usd)
print(f"Cost: ${total_cost:.6f}, duration: {trace.flow_run.duration_ms}ms")

Paused results (tool calls)

If the flow pauses for tool calls and you did not provide a tool_handler, execute() returns a PausedResult instead. Check requires_tool_calls:

result = flow.execute(message="hi", tools=[...])
if result.requires_tool_calls:
    # PausedResult — see "Tool calls" guide for how to resume
    print(result.tool_calls)
else:
    print(result.output)

Async submission (execute_async)

For long-running flows you can submit to a server-side queue and poll for completion:

job = flow.execute_async(message="Long-running input")
# job.execution_id, job.flow_id available immediately
 
status = job.wait(timeout=300)  # blocks until terminal or raises APITimeoutError
print(status.status)            # "completed" | "failed"
print(status.result)            # final output when completed

The _async suffix refers to the server-side execution model (queue-backed), not the client's async/await. Tool calls are not supported on this path.

Error handling

execute() raises typed exceptions on failure. See Errors & retries for the full hierarchy.

from noukai_sdk import FlowNotFoundError, InsufficientCreditsError
 
try:
    result = flow.execute(message="hello")
except FlowNotFoundError:
    print("Flow not found")
except InsufficientCreditsError:
    print("Account out of credits")

Per-call overrides

When your client has org and project defaults, you can still override them per-call using a fully-qualified identifier or kwargs:

from noukai_sdk import Noukai
 
client = Noukai(org="acme", project="spelling")
 
# Use defaults
result = client.flow("grade-3").execute(message="hello")
 
# Override with fully-qualified identifier
result = client.flow("acme/spelling/grade-3").execute(message="hello")
 
# Override with kwargs
result = client.flow(org="acme", project="spelling", slug="grade-3").execute(message="hello")
 
# Switch to different org/project entirely
result = client.flow("example/math/addition").execute(message="1+1")

Next steps