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 BIGINT or INTEGER, 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():

Python
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 bigint columns: Use BigIntIDGenerator. 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 an INTEGER column.
  • Logging and debugging: Use ShortIDGenerator for 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:

Python
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:

Python
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 database

AsyncIDGenerator.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:

Python
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:

  • UUIDGenerator and BigIntIDGenerator are 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:

Python
from tenxgraph.utils.id_generator import ShortIDGenerator

gen = ShortIDGenerator()
print(gen.generate(), gen.id_type)  # e.g. Xy9aBc3d IDType.STRING

For more details on the ID generator API, class hierarchy, and all built-in implementations, see the ID generator reference.

Last updated for v0.10.0Edit this page on GitHubReport an issue