CrewAI Persistence — DynamoDB¶
A CrewAI FlowPersistence backend for Amazon DynamoDB with built-in message pruning. It persists flow state between steps so your flows can resume from any saved checkpoint, and it can automatically cap your message history before each write — no changes to your flow code or state model required.
Current version
crewai-persistence-dynamodb 0.1.0 · Requires Python >=3.10,<3.14 (CrewAI constraint) and crewai>=1.0.0
What it is¶
DynamoDBFlowPersistence subclasses crewai.flow.persistence.base.FlowPersistence (a Pydantic BaseModel + ABC in CrewAI 1.x) and stores CrewAI flow state in a DynamoDB table. Configuration (table_name, region_name, endpoint_url, ttl_seconds, messages_key) is declared as Pydantic fields, while the runtime objects (the boto3 session/clients and the reducer) are held as PrivateAttr. It accepts an optional MessageReducer that prunes the message list at the persistence layer.
Persistence is keyed by the flow state's id field — the flow_uuid. One item is stored per flow run: each save upserts the latest state via put_item, so load_state returns the most recent. Flow state is serialised to a JSON string in a data attribute (which sidesteps DynamoDB's float/Decimal and empty-value quirks). save_state accepts either a Pydantic BaseModel or a plain dict; load_state returns a dict (or None).
Installation¶
The [reducer] extra pulls in agentstate-reducer, required only if you pass a reducer.
Table setup¶
| Behaviour | Detail |
|---|---|
| Table creation | Created automatically if absent, with PAY_PER_REQUEST billing |
| Key schema | Partition (HASH) key flow_uuid of type S (string) |
| TTL | If ttl_seconds is set, TTL is enabled on the ttl attribute at creation time |
The backend calls init_db() from __init__: it tries to load() the table, and if it does not exist (ResourceNotFoundException), creates it and waits until it is active.
IAM permissions
Auto-creation needs dynamodb:CreateTable, dynamodb:DescribeTable, and (for TTL) dynamodb:UpdateTimeToLive. If the table is pre-provisioned, dynamodb:GetItem and dynamodb:PutItem are sufficient.
Authentication¶
DynamoDB uses the standard AWS credential chain via boto3. Provide credentials through the environment, a shared profile, or an explicit boto_session.
Omit static keys. On EC2/ECS/Lambda the attached IAM role is resolved automatically by boto3's default credential chain.
Quick start¶
Normal flow¶
from crewai.flow.flow import Flow, start, listen
from crewai.flow.persistence import persist
from crewai_persistence_dynamodb import DynamoDBFlowPersistence
persistence = DynamoDBFlowPersistence(
table_name="crewai_flow_states",
region_name="us-east-1",
)
@persist(persistence)
class MyFlow(Flow):
@start()
def first_step(self):
return {"status": "started", "value": 42}
@listen(first_step)
def second_step(self, data):
return data
flow = MyFlow()
flow.kickoff()
Conversational flow (with message history and pruning)¶
from crewai.flow.flow import Flow, start, listen
from crewai.flow.persistence import persist
from agentstate_reducer import MessageReducer
from agentstate_reducer.models import ReducerConfig
from crewai_persistence_dynamodb import DynamoDBFlowPersistence
reducer = MessageReducer(config=ReducerConfig(min_messages=10, max_messages=20))
persistence = DynamoDBFlowPersistence(
table_name="crewai_flow_states",
region_name="us-east-1",
reducer=reducer, # prune before each save
messages_key="messages", # state key holding the message list (default)
)
@persist(persistence)
class ChatFlow(Flow):
@start()
def handle_turn(self):
# messages accumulate here; pruning happens automatically at save time
messages = self.state.get("messages", [])
messages.append({"role": "human", "content": "Tell me about DynamoDB."})
# ... call your LLM here ...
messages.append({"role": "ai", "content": "DynamoDB is a managed NoSQL key-value store..."})
return {"messages": messages}
flow = ChatFlow()
flow.kickoff()
The @persist decorator
@persist is imported from crewai.flow.persistence. Applied to a Flow subclass with your persistence instance, it transparently calls save_state / load_state keyed by the flow state's id (the flow_uuid).
API reference¶
DynamoDBFlowPersistence(table_name, *, region_name=None, boto_session=None, endpoint_url=None, ttl_seconds=None, reducer=None, messages_key="messages")¶
| Parameter | Type | Default | Description |
|---|---|---|---|
table_name |
str |
required | DynamoDB table name (created if absent) |
region_name |
str \| None |
None |
AWS region for the boto3 session |
boto_session |
boto3.Session \| None |
None |
Pre-configured boto3 session; if omitted, one is built from region_name |
endpoint_url |
str \| None |
None |
Custom endpoint (e.g. DynamoDB Local) |
ttl_seconds |
int \| None |
None |
If set, items expire after this many seconds (TTL enabled on the ttl attribute) |
reducer |
MessageReducer \| None |
None |
Optional pruner — see Built-in message pruning |
messages_key |
str |
"messages" |
State key that holds the message list |
Methods¶
| Method | Description |
|---|---|
init_db() |
Create the boto3 clients and ensure the table exists (called automatically by __init__) |
save_state(flow_uuid, method_name, state_data) |
Persist flow state (upsert by flow_uuid via put_item); accepts a BaseModel or dict |
load_state(flow_uuid) |
Load the most recently saved state as a dict (consistent read); returns None if not found |
Built-in message pruning¶
Long-running conversational flows accumulate message history with every turn, inflating item size, increasing DynamoDB storage/throughput costs, and eventually blowing past LLM context limits.
Pass a MessageReducer and the backend prunes the message list inside save_state() before the item is written to DynamoDB. Your flow code and state model stay untouched.
When len(messages) > max_messages, the oldest human/ai messages are removed until min_messages remain. System-prompt index 0, system/function messages, and tool messages (unless their parent ai message is pruned) are preserved.
Full reducer configuration
For preserve_first, cascade_tool_messages, summarize_fn, token budgeting, and role aliases, see the reducer overview and token budget docs.
Data model¶
Each call to save_state upserts a single DynamoDB item keyed by flow_uuid. Only the latest state for each flow run is stored (put_item overwrites on the same key).
| Attribute | Description |
|---|---|
flow_uuid |
Unique identifier for the flow run (partition key) |
data |
The full state dict serialised as a JSON string |
method_name |
Name of the flow method that triggered the save |
saved_at |
ISO-8601 UTC timestamp of the save |
ttl |
(Only when ttl_seconds is set) Unix epoch expiry time |
JSON serialisation
State is stored as a JSON string rather than native DynamoDB attributes, avoiding DynamoDB's float/Decimal conversion and empty-string/empty-set restrictions. load_state parses data back into a dict.