# Configure ID generators

> Control the format of thread IDs and run IDs using built-in or custom generators to match your storage backend and observability requirements.

Source: https://10xgraph.com/docs/guides/configure-id-generator
Last updated: 2026-10-08

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](/docs/reference/python/id-generator).
