Configure ID generators
In shortControl the format of thread IDs and run IDs using built-in or custom generators to match your storage backend and observability requirements.
- 6 min read
- 10 sections
- Updated
- v0.10.0
- Markdown
By default, 10xGraph generates UUID v4 strings for threads, runs and messages. UUIDs are collision-resistant and require no coordination, but they are not sortable by time and may not fit your database schema (if you have integer primary keys) or your observability system (if you prefer short, human-readable codes). You can swap the ID generator to match your infrastructure.
This guide shows when and how to choose or build an ID generator, from built-in options to custom async strategies.
Why change your ID generator
10xGraph creates these IDs when you do not supply them:
- Thread IDs: Identify a persistent conversation thread. Threads have memory; the same thread ID resumes from the last message.
- Run IDs: Identify a single invocation. If your config has no
run_id, a new one is generated. - Message IDs: Messages created without an ID also use the generator.
All are generated by the same id_generator instance. A thread_id or run_id you pass in config is used as given. Here are the reasons to customize:
- Database schema. If your primary key is
BIGINTorINTEGER, UUIDs waste space and slow down lookups. - Time ordering. If you want to find the oldest or newest threads without a separate timestamp column, use a time-sortable generator.
- Observability. Short, human-readable IDs (8 characters) make logs and dashboards easier to scan.
- External systems. Some APIs or databases require specific ID formats (prefixed strings, specific lengths, custom encodings).
Built-in generators at a glance
| Generator | Type | Example | Best for |
|---|---|---|---|
UUIDGenerator |
STRING |
550e8400-e29b-41d4-... |
Default choice; maximum uniqueness. |
BigIntIDGenerator |
BIGINT |
1712576400000000000 |
PostgreSQL bigint columns; time-sortable. |
TimestampIDGenerator |
INTEGER |
1712576400123456 |
Smaller footprint than BigInt; still sortable. |
IntIDGenerator |
INTEGER |
2147483647 |
Random 32-bit integers (0 to 4,294,967,295); low-volume use only. |
HexIDGenerator |
STRING |
1a2b3c4d5e6f7890... |
32-char hex strings without hyphens. |
ShortIDGenerator |
STRING |
Ab3XyZ9k |
Human-readable 8-char codes for URLs and logs. |
DefaultIDGenerator |
STRING |
(empty string) | Returns an empty string, so the framework falls back to its own UUID. |
All are imported from tenxgraph.utils.id_generator or the top-level tenxgraph.utils.
Quick start: use a built-in generator
To replace the default UUID generator, pass id_generator to StateGraph():
from tenxgraph.core.graph import StateGraph
from tenxgraph.core.state import Message
from tenxgraph.utils.constants import END
from tenxgraph.utils.id_generator import BigIntIDGenerator
def echo(state):
return Message.text_message("Hello back", role="assistant")
# Create a graph with timestamp-sortable integer IDs
graph = StateGraph(id_generator=BigIntIDGenerator())
graph.add_node("echo", echo)
graph.set_entry_point("echo")
graph.add_edge("echo", END)
app = graph.compile()
# Invoke without a thread_id; the generator supplies it (a 19-digit integer)
result = app.invoke({"messages": [Message.text_message("Hello")]})Threads, runs and messages created without an explicit ID now use the new ID format. The generator does not affect how you invoke the graph.
Choosing the right generator
Choose based on your backend and observability needs:
- Unknown or mixed backends: Use
UUIDGenerator(the default). It works everywhere. - PostgreSQL with
bigintcolumns: UseBigIntIDGenerator. It is sortable by time and occupies a native column type. - Need to sort by creation time but have space constraints: Use
TimestampIDGenerator. It is 16 digits, still sortable, and fits anINTEGERcolumn. - Logging and debugging: Use
ShortIDGeneratorfor human-readable IDs in logs. Warn: do not use as a primary key in high-volume systems (62^8 ≈ 218 trillion combinations, not cryptographically unique). - Custom or legacy database schema: Write a custom generator (see below).
Write a synchronous custom generator
Extend BaseIDGenerator to implement your own ID logic:
import uuid
from tenxgraph.core.graph import StateGraph
from tenxgraph.utils.id_generator import BaseIDGenerator, IDType
class PrefixedUUIDGenerator(BaseIDGenerator):
"""Generates UUIDs with a prefix for filtering, e.g., 'run_550e8400-e29b-...'."""
def __init__(self, prefix: str = "run"):
self.prefix = prefix
@property
def id_type(self) -> IDType:
return IDType.STRING
def generate(self) -> str:
return f"{self.prefix}_{uuid.uuid4()}"
# Use it:
graph = StateGraph(id_generator=PrefixedUUIDGenerator("sess"))
app = graph.compile()Your generator receives no input and must produce a new, unique ID each time generate() is called. generate() may return str, int, or an awaitable (see the async section).
Write an asynchronous custom generator
If ID generation involves async I/O (e.g., fetching from a database), extend AsyncIDGenerator:
import asyncpg
from tenxgraph.core.graph import StateGraph
from tenxgraph.utils.id_generator import AsyncIDGenerator, IDType
class DatabaseSequenceGenerator(AsyncIDGenerator):
"""Fetch the next ID from a Postgres sequence."""
def __init__(self, pool):
"""
Args:
pool: An asyncpg connection pool.
"""
self.pool = pool
@property
def id_type(self) -> IDType:
return IDType.BIGINT
async def generate(self) -> int:
async with self.pool.acquire() as conn:
return await conn.fetchval("SELECT nextval('id_sequence')")
# Use it with your graph:
async def main():
pool = await asyncpg.create_pool("postgresql://user:pass@localhost/db")
graph = StateGraph(id_generator=DatabaseSequenceGenerator(pool))
app = graph.compile()
# Now each invoke fetches the next ID from the databaseAsyncIDGenerator.generate() returns an awaitable, and the framework resolves it when it generates message IDs. Test the async path with your own thread and run IDs before relying on it: thread_id and run_id defaults are read from the same factory without being awaited, so pass explicit values in config when using an async generator. Use this for IDs from an external sequence or counter service.
Access the generated ID inside a node
The generator is bound in the dependency container, so a node can inject a freshly generated ID and its type:
from injectq import Inject
from tenxgraph.core.graph import StateGraph
from tenxgraph.utils.constants import END
from tenxgraph.utils.id_generator import ShortIDGenerator
async def my_node(
state,
config: dict,
generated_id: str = Inject["generated_id"],
generated_id_type: str = Inject["generated_id_type"],
):
"""
Log or use the generated ID.
Args:
generated_id: A new ID from the configured generator.
generated_id_type: The IDType value (e.g., 'string', 'integer', 'bigint').
"""
print(f"New ID: {generated_id} (type: {generated_id_type})")
return state
graph = StateGraph(id_generator=ShortIDGenerator())
graph.add_node("my_node", my_node)
graph.set_entry_point("my_node")
graph.add_edge("my_node", END)
app = graph.compile()generated_id is a new value from the generator on each resolution, not the current run’s ID. Read the current IDs from config["thread_id"] and config["run_id"].
Idempotency and ID consistency
The same generator instance is reused across multiple invocations. If your generator has state (e.g., a database connection or counter), ensure it is thread-safe and produces globally unique IDs. For example:
UUIDGeneratorandBigIntIDGeneratorare stateless and safe.- Custom generators with database connections should use connection pooling.
- Generators that maintain in-memory counters are safe only in single-threaded contexts (a development server).
For production multi-worker deployments, use a stateless generator or one backed by a shared coordinator (database sequence, Redis counter, distributed ID service).
Common problems and fixes
| Problem | Cause | Solution |
|---|---|---|
| IDs collide in high throughput | BigIntIDGenerator and TimestampIDGenerator derive IDs from the clock, so two calls within the clock resolution can produce the same ID. |
Use UUIDGenerator or another random generator. |
UNIQUE constraint violation |
IntIDGenerator (32-bit) has collisions at scale, or ShortIDGenerator used as a primary key in high-volume systems. |
Use BigIntIDGenerator or UUIDGenerator. Reserve ShortIDGenerator for human-readable IDs only, not as a DB primary key. |
| Database column type mismatch | Generator returns int but the column is VARCHAR, or vice versa. |
Match the id_type property to your schema. For example, use BigIntIDGenerator (returns BIGINT) for a bigint column. |
| Empty IDs from the generator | DefaultIDGenerator.generate() returns an empty string by design. |
Use UUIDGenerator() or another concrete generator when you call the generator yourself. |
| Custom generator is slow | Database query or API call in async def generate() is blocking the critical path. |
Optimize the query or API (add indices, cache, use Redis), or pre-allocate IDs to reduce latency. |
Verify your configuration
To confirm a generator produces what you expect, call it directly:
from tenxgraph.utils.id_generator import ShortIDGenerator
gen = ShortIDGenerator()
print(gen.generate(), gen.id_type) # e.g. Xy9aBc3d IDType.STRINGFor more details on the ID generator API, class hierarchy, and all built-in implementations, see the ID generator reference.