Skip to main content
A runnable is any unit of computation that takes an input and returns an output. Models, parsers, tools, and plain functions are all runnables. You compose them with the | operator into a pipeline, and run that pipeline sync or async with one call.

The runnable contract

Every runnable implements the same small protocol, defined in infy/core.py. That is the entire contract:
Because it is a Protocol, anything with these methods is a runnable. There is no base class to inherit from.
Every method takes an optional ctx (a Context). Leave it out and the runnable uses defaults. See Context below.

Piping into a Sequence

The | operator chains runnables left to right into a Sequence. Each step receives the previous step’s output.
Sequence runs its steps in order, threading the result through:
ainvoke awaits each step in turn, so an async chain never blocks the event loop. Sequence also flattens: piping two sequences together produces one flat Sequence, not a nested one.

Automatic coercion

You rarely construct runnables by hand. When you pipe something that is not already a runnable, coerce (in infy/core.py) converts it: So a bare function drops straight into a chain:
The function is wrapped in a Lambda automatically. Piping a callable that coerce cannot handle raises TypeError.

Lambda

Lambda wraps a synchronous function as a runnable. Its invoke calls the function directly; its ainvoke runs the function in an executor so a sync function does not block the loop.
For a coroutine function, use AsyncLambda, whose ainvoke awaits the function directly. The async coercion helper acoerce picks AsyncLambda automatically for coroutine functions.
Use Lambda for pure sync work and AsyncLambda when your function is async def. Both compose with | exactly like any other runnable.

Parallel: fan out on one input

A dict in a chain becomes a Parallel. Each value is coerced to a runnable, every branch receives the same input, and the result is a dict with the same keys.
You can write the same thing as a plain dict and let coercion build the Parallel for you:
Branches run concurrently. invoke runs them across a ThreadPoolExecutor; ainvoke runs them under asyncio.gather. Both honor max_concurrency from the Context when set.
Parallel returns a dict keyed by branch name. The next step in a chain receives that whole dict, so a downstream runnable must expect a dict input.

Context and concurrency

Every runnable method accepts an optional Context. It carries tags, metadata, and max_concurrency, and passes down through a chain.
For Parallel, max_concurrency caps how many branches run at once (a semaphore under ainvoke, the thread-pool size under invoke). Leave ctx as None and each Parallel defaults to running all of its branches at once.

Streaming through a chain

Runnables also expose stream and astream. A Sequence streams by piping each step’s output into the next, yielding chunks as they arrive.

Chat models

The ChatModel protocol every provider implements, and how it plugs into a chain.

Structured output

Parse and validate model output with JsonParser or a pydantic model.