Skip to content

Managing State

Often, you’ll have some preset shape that you want to use as the state for your workflow. The best way to do this is to use a Pydantic model to define the state. This way, you:

  • Get type hints for your state
  • Get automatic validation of your state
  • (Optionally) Have full control over the serialization and deserialization of your state using validators and serializers

NOTE: You should use a pydantic model that has defaults for all fields. This enables the Context object to automatically initialize the state with the defaults.

Here’s a quick example of how you can leverage workflows + pydantic to take advantage of all these features:

from pydantic import BaseModel, Field, field_validator, field_serializer
from typing import Union
# This is a random object that we want to use in our state
class MyRandomObject:
def __init__(self, name: str = "default"):
self.name = name
# This is our state model
# NOTE: all fields must have defaults
class MyState(BaseModel):
model_config = {"arbitrary_types_allowed": True}
my_obj: MyRandomObject = Field(default_factory=MyRandomObject)
some_key: str = Field(default="some_value")
# This is optional, but can be useful if you want to control the serialization of your state!
@field_serializer("my_obj", when_used="always")
def serialize_my_obj(self, my_obj: MyRandomObject) -> str:
return my_obj.name
@field_validator("my_obj", mode="before")
@classmethod
def deserialize_my_obj(
cls, v: Union[str, MyRandomObject]
) -> MyRandomObject:
if isinstance(v, MyRandomObject):
return v
if isinstance(v, str):
return MyRandomObject(v)
raise ValueError(f"Invalid type for my_obj: {type(v)}")

Then, simply annotate your workflow state with the state model:

from llama_index.core.workflow import (
Context,
StartEvent,
StopEvent,
Workflow,
step,
)
class MyWorkflow(Workflow):
@step
async def start(self, ctx: Context[MyState], ev: StartEvent) -> StopEvent:
# Allows for atomic state updates
async with ctx.store.edit_state() as ctx_state:
ctx_state["state"]["my_obj"]["name"] = "new_name"
# Can also access fields directly if needed
name = await ctx.store.get("my_obj.name")
return StopEvent(result="Done!")

As you have seen, workflows have a Context object that can be used to maintain state across steps.

If you want to maintain state across multiple runs of a workflow, you can pass a previous context into the .run() method.

handler = w.run()
result = await handler
# continue with next run
handler = w.run(ctx=handler.ctx)
result = await handler