Threads
A thread is an instance of an agent — the runtime execution context where conversations happen. Each thread has isolated storage for messages and files, maintains its own conversation history, and can be handed off between agents while preserving full context.
This page covers the core ideas: what a thread is, its identity, the ThreadState interface, and the small primitives that live directly on it (resource loading, tool invocation, effects, events, context, runtime context, subagent hierarchy, lifecycle). Larger surfaces have their own pages:
- Messages — reading, injecting, queuing, updating, deleting
- File System — per-thread file storage, streaming, search
- Code Execution — isolated JS/TS sandbox via
runCode - Execution State — step counts, turn control, cancellation
- Thread Endpoints — HTTP APIs that receive a
ThreadState
1. Thread Definition
A thread is an instance of an agent with isolated storage for messages and files.
1.1 Agent Assignment
The agent assigned to a thread can change over time while preserving all history and context. This is called a handoff.
| Scenario | Behavior |
|---|---|
| Tool handoff | Calling an ai_human agent as a tool transfers control to that agent |
| Manual handoff | Queue a tool call to an agent to trigger handoff programmatically |
| dual_ai execution | Creates its own isolated thread instance (no handoff) |
Handoffs enable workflows where specialized agents handle different parts of a conversation — a triage agent routes to support, support escalates to billing — all within the same thread with full context preserved.
1.2 ThreadState Interface
ThreadState is the unified interface for all thread operations. It provides a consistent API for interacting with threads regardless of context — whether from tools during execution, hooks, or HTTP endpoints.
1.2.1 Properties
| Property | Type | Description |
|---|---|---|
threadId | string | Unique thread identifier (readonly) |
agentId | string | Owning agent name (readonly) |
userId | string | null | Associated user (readonly) |
createdAt | number | Creation timestamp in microseconds (readonly) |
children | SubagentRegistryEntry[] | Resumable child subagent registry (readonly) |
terminated | number | null | Termination timestamp in microseconds, or null when active (readonly) |
arguments | Record<string, unknown> | Arguments supplied when this thread was created or invoked. Resumable subagent initial arguments appear here. See Thread Arguments |
context | Record<string, unknown> | Mutable per-execution scratch storage. See Context Storage |
execution | ExecutionState | null | Execution state during active execution, null at rest. See Execution State |
_notPackableRuntimeContext | Record<string, unknown> | undefined | Runtime-specific context (non-portable). See Runtime Context |
1.2.2 Methods
The methods directory below is grouped by topic. Topics with their own page link out; the rest are documented on this page.
Messages — Messages →
| Method | Returns | Description |
|---|---|---|
getMessages(options?) | Promise<MessagesResult> | Get messages with pagination |
getMessage(messageId) | Promise<Message | null> | Get single message by ID |
injectMessage(input) | Promise<Message> | Inject a new message |
queueMessage(input) | Promise<void> | Queue a message for delivery on next step |
updateMessage(messageId, updates) | Promise<Message> | Update existing message |
deleteMessage(messageId) | Promise<boolean> | Delete a message |
File System — File System →
| Method | Returns | Description |
|---|---|---|
writeFile(path, data, mimeType, options?) | Promise<FileRecord> | Write file |
readFile(path) | Promise<ArrayBuffer | null> | Read file content |
readFileStream(path, options?) | Promise<AsyncIterable<FileChunk> | null> | Stream large files |
statFile(path) | Promise<FileRecord | null> | Get file metadata |
readdirFile(path) | Promise<ReaddirResult> | List directory |
unlinkFile(path) | Promise<void> | Delete file |
mkdirFile(path) | Promise<FileRecord> | Create directory |
rmdirFile(path) | Promise<void> | Remove directory |
getFileStats() | Promise<FileStats> | Get filesystem stats |
grepFiles(pattern) | Promise<GrepResult[]> | Search file contents |
findFiles(pattern) | Promise<FindResult> | Find files by glob |
getFileThumbnail(path) | Promise<ArrayBuffer | null> | Get image thumbnail |
Code Execution — Code Execution →
| Method | Returns | Description |
|---|---|---|
runCode(source, options?) | CodeExecution | Execute JS/TS source in an isolated sandbox; returns a thenable handle with terminate() |
Subagent Hierarchy — see §9 Subagent Hierarchy
| Method | Returns | Description |
|---|---|---|
getChildThread(referenceId) | Promise<ThreadState | null> | Resolve child thread by subagent reference |
getParentThread() | Promise<ThreadState | null> | Resolve parent thread for this thread |
notifyParent(content) | Promise<void> | Queue a silent message to this thread’s parent |
setStatus(status) | Promise<void> | Update this thread’s status in the parent registry when present |
stopSession(options?) | Promise<void> | Terminally stop the active agent session; optional notifyParent and status trigger those parent side effects |
Resource Loading — see §2 Resource Loading
| Method | Returns | Description |
|---|---|---|
loadModel(name) | Promise<T> | Load model definition |
loadPrompt(name) | Promise<T> | Load prompt definition |
loadAgent(name) | Promise<T> | Load agent definition |
getPromptNames() | string[] | List available prompts |
getAgentNames() | string[] | List available agents |
getModelNames() | string[] | List available models |
env(propertyName) | Promise<string> | Resolve environment variable using thread/user/instance/agent/prompt precedence |
envType(propertyName) | Promise<'text' | 'secret'> | Resolve display/redaction classification for an environment value |
setEnv(propertyName, value, options?) | Promise<void> | Set a thread environment variable, optionally choosing text or secret, and propagate to active descendants |
Tool Invocation — see §3 Tool Invocation
| Method | Returns | Description |
|---|---|---|
queueTool(toolName, args) | void | Queue tool for async execution |
invokeTool(toolName, args) | Promise<ToolResult> | Invoke tool and wait for result |
Effect Scheduling — see §4 Effect Scheduling
| Method | Returns | Description |
|---|---|---|
scheduleEffect(name, args, delay?) | Promise<string> | Schedule effect, returns effect ID |
getScheduledEffects(name?) | Promise<ScheduledEffect[]> | Get pending effects |
removeScheduledEffect(id) | Promise<boolean> | Cancel scheduled effect |
Events — see §5 Events
| Method | Returns | Description |
|---|---|---|
emit(event, data) | void | Emit event to connected clients |
Durable Key-Value Store — see §7 Durable Key-Value Store
| Method | Returns | Description |
|---|---|---|
getValue(key) | Promise<T | null> | Read a value from the thread’s durable key-value store |
setValue(key, value) | Promise<void> | Write a JSON-serializable value; null/undefined deletes the key |
User identity
| Method | Returns | Description |
|---|---|---|
user() | Promise<ThreadUser | null> | Profile of the thread’s user (identity fields only — id, username, role, permissions, email, display_name, avatar_url; never credentials). null for user-less threads. |
Skills — see §7.4 Agent Skills
| Method | Returns | Description |
|---|---|---|
skills.list(options?) | Promise<SkillMetadata[]> | Installed skills (enabled only unless includeDisabled) |
skills.get(name) | Promise<{ metadata, skillMd } | null> | A skill’s metadata + SKILL.md source |
skills.readFile(name, path) | Promise<string | null> | One bundled file’s text |
skills.install(files) | Promise<SkillMetadata> | Install/replace a skill from its file set |
skills.setEnabled(name, enabled) | Promise<boolean> | Enable/disable an installed skill |
skills.remove(name) | Promise<boolean> | Uninstall a skill |
Lifecycle — see §8 Lifecycle
| Method | Returns | Description |
|---|---|---|
terminate() | Promise<void> | Soft-terminate the thread |
1.3 Access Contexts
ThreadState is provided in different contexts:
| Context | Execution State | Use Case |
|---|---|---|
| Tools | Present | During agent execution |
| Hooks | Present | Lifecycle interception |
| Endpoints | Null | HTTP API access |
2. Resource Loading
2.1 Loading Definitions
ThreadState provides access to registered definitions:
const model = await state.loadModel('gpt-4o');
const prompt = await state.loadPrompt('support-prompt');
const agent = await state.loadAgent('customer-support');
2.2 Listing Resources
const prompts = state.getPromptNames();
const agents = state.getAgentNames();
const models = state.getModelNames();
2.3 Environment Variable Resolution
const apiKey = await state.env('GOOGLE_API_KEY');
await state.setEnv('TOPDOWN_APPROVAL_GATE', 'open');
await state.setEnv('PUBLIC_LABEL', 'QA', { type: 'text' });
await state.setEnv('API_TOKEN', 'secret-token', { type: 'secret' });
env(propertyName) resolves in this order:
- Thread environment variables
- User-account environment variables
- Runtime instance environment variables
- Agent definition defaults (
agent.env) - Prompt definition defaults (
prompt.env)
envType(propertyName) returns the display/redaction classification for a resolved environment value. Unknown or non-thread sources default to secret. Values marked secret should be redacted from tool output and logs; values marked text may be displayed.
setEnv(propertyName, value, options?) writes to thread storage and propagates recursively to active descendants. Pass { type: 'text' } or { type: 'secret' } to choose the display/redaction classification. Omitting type preserves any existing classification for that key, or defaults a new key to secret. During propagation, descendants must filter out inherited values for variable names marked scoped in that descendant’s agent graph. Terminated descendants are skipped.
Thread, user, and instance environment values should be encrypted at rest when the runtime supports encrypted storage.
For resumable subagent creation, runtimes may require missing scoped variables to be provided through a pending request flow before boot can continue. See Agents §7.9.
3. Tool Invocation
3.1 Queuing Tools
Queue a tool for asynchronous execution:
state.queueTool('send_email', {
to: 'user@example.com',
subject: 'Hello',
body: 'Message content',
});
If the thread is currently executing, the tool is added to the queue. Otherwise, a new execution is started.
3.2 Direct Invocation
Invoke a tool and wait for the result:
const result = await state.invokeTool('get_weather', {
location: 'San Francisco',
});
if (result.status === 'success') {
console.log(result.result);
}
4. Effect Scheduling
Effects are scheduled operations that run outside the normal tool execution context. Unlike tools which execute immediately during conversation, effects:
- Run independently of the conversation flow
- Can be scheduled with delays
- Execute with their own ThreadState context
- Are ideal for side effects that don’t need immediate results
Effect definition syntax is specified in Effects. Packed effects may use qualified names ({packageId}/{effectName}) to avoid collisions.
4.1 Scheduling Effects
const effectId = await state.scheduleEffect(
'send_reminder_email',
{ to: 'user@example.com', subject: 'Reminder' },
30 * 60 * 1000 // delay in milliseconds
);
| Parameter | Type | Description |
|---|---|---|
name | string | Effect name (file in agents/effects/) |
args | Record<string, unknown> | Arguments passed to effect handler |
delay | number | Delay in milliseconds (default: 0 for immediate) |
Returns a unique effect ID (UUID) that can be used to cancel the effect.
4.2 ScheduledEffect Record
| Property | Type | Description |
|---|---|---|
id | string | Unique effect ID (UUID) |
name | string | Effect name |
args | Record<string, unknown> | Arguments to be passed |
scheduledAt | number | Scheduled execution time (microseconds) |
createdAt | number | When effect was scheduled (microseconds) |
4.3 Managing Scheduled Effects
const effects = await state.getScheduledEffects();
const reminders = await state.getScheduledEffects('send_reminder_email');
const removed = await state.removeScheduledEffect(effectId);
if (removed) {
console.log('Effect cancelled');
}
5. Events
5.1 Emitting Events
Send custom events to connected clients:
state.emit('progress', {
step: 3,
total: 10,
message: 'Processing data...',
});
Events are delivered via WebSocket to any connected clients.
5.2 Event Guidelines
- Event names SHOULD be lowercase with underscores
- Event data MUST be JSON-serializable
- Events SHOULD NOT contain sensitive information
6. Thread Arguments and Context Storage
6.1 Thread Arguments
arguments is the read-only argument object supplied when the thread was
created or invoked.
For resumable subagent threads, runtimes MUST restore the creation-time
subagent_create.arguments object into ThreadState.arguments on every run and
when exposing the child through endpoints, hooks, effects, or other
ThreadState access. For dual_ai child threads, both sides see the same
argument object:
const initialArgs = state.arguments;
const shirtSize = state.arguments.shirt_size;
Runtimes should keep the durable copy with child thread metadata/properties.
They should also expose top-level argument keys to prompt variable interpolation,
for whichever child side is currently rendering, including included prompts,
without requiring callers to put those values in state.context.
6.2 Per-Execution Context
The context property is mutable execution-scoped scratch storage for values
that should be shared while the current agent run is being assembled and
executed. It is used for:
- sharing derived values between tools, hooks, effects, and runtime lifecycle code during one execution
- runtime-internal coordination data that should not become a thread argument
For example:
state.context.userData = { id: 123, name: 'Alice' };
// Retrieve in another tool call
const user = state.context.userData;
Runtimes may seed context at execution start with runtime coordination data.
User code may mutate it during execution, and later
participants in that same execution see those mutations.
Invocation or creation arguments belong in arguments,
not in context.
6.3 Context Limitations
| Aspect | Behavior |
|---|---|
| Scope | Single execution only |
| Persistence | Mutations are lost after execution ends |
| Type | Record<string, unknown> |
| Size | No enforced limit |
For persistent storage across executions, use the durable key-value store, the file system, or external storage.
7. Durable Key-Value Store
getValue and setValue provide a simple durable key-value store scoped to the thread. Values persist across executions, tool calls, hook invocations, and endpoint requests for the lifetime of the thread.
It is one of two ways to store persistent data on a thread, alongside the file system.
7.1 Reading and Writing Values
const count = (await state.getValue<number>('invocation_count')) ?? 0;
await state.setValue('invocation_count', count + 1);
await state.setValue('preferences', { theme: 'dark', locale: 'en-US' });
const prefs = await state.getValue<{ theme: string; locale: string }>('preferences');
7.2 Semantics
| Aspect | Behavior |
|---|---|
| Scope | Per-thread; not shared with parent, child, or sibling threads |
| Persistence | Durable across executions for the lifetime of the thread |
| Value type | Any JSON-serializable value (objects, arrays, strings, numbers, booleans, null) |
| Missing keys | getValue returns null |
| Deletion | setValue(key, null) or setValue(key, undefined) deletes the key |
| Subagents | Values are not automatically copied to spawned child threads |
7.3 Comparison With Other Storage
| Surface | Scope | Persistence | Intended Use |
|---|---|---|---|
arguments | Thread invocation | Durable when backed by thread metadata/properties | Creation/invocation arguments exposed to thread code and prompt interpolation |
context | Single execution | Ephemeral | Mutable scratch state shared during one execution |
getValue / setValue | Thread | Durable | Small structured state scoped to the thread |
env / envType / setEnv | Thread → user → instance → agent → prompt | Durable (encrypted at rest when supported) | Configuration and secrets with layered resolution and display/redaction metadata |
| File system | Thread | Durable | Large or binary payloads |
Runtime semantics and conformance requirements for the durable key-value store live in Runtime §15.
7.4 Agent Skills
state.skills exposes the instance-global Agent Skills library — the one deliberate exception to per-thread scoping in this section. Skills follow the Agent Skills standard: a folder whose root SKILL.md opens with YAML frontmatter carrying a required name (lowercase kebab-case, ≤64 chars) and description (≤1024 chars), optionally accompanied by scripts, references, and assets.
Requirements:
- Skills are stored server-side and installed once for the whole instance; every thread sees the same library. Runtimes MUST NOT require client-side storage for skill content.
- A skill’s identity MUST come from its
SKILL.mdfrontmatter — never from a caller-supplied name — and installs MUST validate the frontmatter against the standard. - Runtimes SHOULD surface skills to agents with progressive disclosure: only
name+descriptionride in ambient context; theSKILL.mdbody (and bundled files) load on demand when the agent judges the skill applicable. - Disabled skills stay installed but MUST be hidden from ambient agent context.
const available = await state.skills.list();
const skill = await state.skills.get('pdf-processing');
const script = await state.skills.readFile('pdf-processing', 'scripts/extract.py');
await state.skills.install([{ path: 'SKILL.md', content: '---\nname: commit-style\ndescription: ...\n---\n...' }]);
8. Lifecycle
terminate() is a soft shutdown:
- Marks the thread inactive and sets
terminated - Aborts in-flight execution
- Rejects future
queueMessage()and execution attempts - Parent registries should reflect
'terminated'status
await state.terminate();
9. Subagent Hierarchy, Communication, and Termination
Subagent-aware runtimes expose parent/child thread relationships through ThreadState:
const child = await state.getChildThread(referenceId);
const parent = await state.getParentThread();
children entries track resumable child references and status/projection metadata:
interface SubagentRegistryEntry {
reference: string;
name: string;
title?: string;
description: string;
resumable?: boolean;
blocking?: boolean;
threadName?: string;
initialArguments?: Record<string, unknown>;
spawnGroupId?: string;
createdAt?: number;
status: string;
parentCommunication?: 'implicit' | 'explicit';
}
initialArguments stores the structured values supplied at child creation. The registry copy is parent-visible metadata; the durable child-side source should live with the child thread’s own metadata/properties and be exposed to both child sides as ThreadState.arguments and prompt interpolation variables.
See Agents §7 for the full lifecycle, communication modes, and cross-thread semantics.
9.1 Cross-Thread Attachment Copying
When communication crosses thread boundaries, filesystem references must be copied:
- parent → child for invocation/messaging attachments
- child → parent for completion/failure payload attachments
Returned attachment paths must be valid in the receiving thread filesystem.
9.2 Lifecycle Status Messages
Runtimes may persist synthesized status messages (for example, system messages tagged with metadata.status_kind) to make cross-thread lifecycle events observable in message feeds.
9.3 Parent Messaging
Children can push information up the hierarchy without waiting for completion:
await state.notifyParent('halfway through the batch');
await state.setStatus('running');
await state.stopSession({
result: 'waiting for input',
notifyParent: 'Please answer the clarification question',
status: 'waiting for clarification',
});
stopSession() is distinct from state.execution.stop(): it marks the active agent session as terminal for lifecycle/subagent purposes. By default it only stops the session; notifyParent and status are optional side effects.
If a child has no parent thread, notifyParent() does not queue anything externally; setStatus() similarly becomes a no-op when there is no parent registry entry. Clients can still render the originating tool call or log entry as outbound parent communication.
10. Runtime Context (Non-Portable)
The _notPackableRuntimeContext property allows runtime implementations to inject platform-specific context.
| Property | Type | Description |
|---|---|---|
_notPackableRuntimeContext | Record<string, unknown> | undefined | Runtime-specific context injected by the implementation |
10.1 Portability
Tools that access _notPackableRuntimeContext cannot be packed, shared, or published to tool registries.
10.2 Example: Accessing a Host-Provided Binding
import { defineTool } from '@standardagents/builder';
import { z } from 'zod';
export default defineTool({
description: 'Query internal database',
args: z.object({ userId: z.string() }),
execute: async (state, args) => {
const env = state._notPackableRuntimeContext?.env as HostEnv | undefined;
if (!env?.DATABASE) {
return { status: 'error', error: 'Database not available' };
}
const result = await env.DATABASE
.prepare('SELECT * FROM users WHERE id = ?')
.bind(args.userId)
.first();
return { status: 'success', result: JSON.stringify(result) };
},
});
Tip: The shape of
_notPackableRuntimeContextis runtime-defined. Typical implementations expose either a handle to host bindings (databases, key-value stores, object storage) underenv, or a process/environment handle for server-style runtimes. Consult your runtime’s documentation for the specific contract.
11. Conformance Requirements
11.1 Thread Lifecycle
- Creation: Thread created with metadata
- Execution: Agent processes messages
- At Rest: Thread accessible via endpoints
- Resumption: New execution on next message
11.2 Platform Independence
ThreadState is an abstract interface. Implementations MUST NOT expose:
- Storage implementation details
- Platform-specific bindings
- Internal state management
11.3 Error Handling
Methods that return promises SHOULD throw on:
- Resource not found (loadModel, loadPrompt, loadAgent)
- Invalid parameters
- Storage errors
Methods SHOULD NOT throw on:
- Empty results (return empty arrays/null instead)
- Missing optional data
12. TypeScript Reference
/**
* Thread state interface - the unified API for thread interactions.
* Available to tools, hooks, and endpoints.
*/
interface ThreadState {
// ─────────────────────────────────────────────────────────────────────────
// Identity (readonly)
// ─────────────────────────────────────────────────────────────────────────
readonly threadId: string;
readonly agentId: string;
readonly userId: string | null;
readonly createdAt: number;
readonly children: SubagentRegistryEntry[];
readonly terminated: number | null;
readonly arguments: Record<string, unknown>;
// ─────────────────────────────────────────────────────────────────────────
// Messages — see /threads/messages
// ─────────────────────────────────────────────────────────────────────────
getMessages(options?: GetMessagesOptions): Promise<MessagesResult>;
getMessage(messageId: string): Promise<Message | null>;
injectMessage(input: InjectMessageInput): Promise<Message>;
queueMessage(input: QueueMessageInput): Promise<void>;
updateMessage(messageId: string, updates: MessageUpdates): Promise<Message>;
deleteMessage(messageId: string): Promise<boolean>;
// ─────────────────────────────────────────────────────────────────────────
// Resource Loading
// ─────────────────────────────────────────────────────────────────────────
loadModel<T = unknown>(name: string): Promise<T>;
loadPrompt<T = unknown>(name: string): Promise<T>;
loadAgent<T = unknown>(name: string): Promise<T>;
getChildThread(referenceId: string): Promise<ThreadState | null>;
getParentThread(): Promise<ThreadState | null>;
getPromptNames(): string[];
getAgentNames(): string[];
getModelNames(): string[];
env(propertyName: string): Promise<string>;
envType(propertyName: string): Promise<'text' | 'secret'>;
setEnv(
propertyName: string,
value: string,
options?: { type?: 'text' | 'secret' }
): Promise<void>;
notifyParent(content: string): Promise<void>;
setStatus(status: string): Promise<void>;
stopSession(options?: {
result?: string;
notifyParent?: string;
status?: string;
}): Promise<void>;
// ─────────────────────────────────────────────────────────────────────────
// Tool Invocation
// ─────────────────────────────────────────────────────────────────────────
queueTool(toolName: string, args: Record<string, unknown>): void;
invokeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
// ─────────────────────────────────────────────────────────────────────────
// Effect Scheduling
// ─────────────────────────────────────────────────────────────────────────
scheduleEffect(name: string, args: Record<string, unknown>, delay?: number): Promise<string>;
getScheduledEffects(name?: string): Promise<ScheduledEffect[]>;
removeScheduledEffect(id: string): Promise<boolean>;
// ─────────────────────────────────────────────────────────────────────────
// Events
// ─────────────────────────────────────────────────────────────────────────
emit(event: string, data: unknown): void;
// ─────────────────────────────────────────────────────────────────────────
// Context Storage
// ─────────────────────────────────────────────────────────────────────────
context: Record<string, unknown>;
// ─────────────────────────────────────────────────────────────────────────
// Durable Key-Value Store
// ─────────────────────────────────────────────────────────────────────────
getValue<T = unknown>(key: string): Promise<T | null>;
setValue(key: string, value: unknown): Promise<void>;
// ─────────────────────────────────────────────────────────────────────────
// File System — see /threads/filesystem
// ─────────────────────────────────────────────────────────────────────────
writeFile(path: string, data: ArrayBuffer | string, mimeType: string, options?: WriteFileOptions): Promise<FileRecord>;
readFile(path: string): Promise<ArrayBuffer | null>;
readFileStream(path: string, options?: ReadFileStreamOptions): Promise<AsyncIterable<FileChunk> | null>;
statFile(path: string): Promise<FileRecord | null>;
readdirFile(path: string): Promise<ReaddirResult>;
unlinkFile(path: string): Promise<void>;
mkdirFile(path: string): Promise<FileRecord>;
rmdirFile(path: string): Promise<void>;
getFileStats(): Promise<FileStats>;
grepFiles(pattern: string): Promise<GrepResult[]>;
findFiles(pattern: string): Promise<FindResult>;
getFileThumbnail(path: string): Promise<ArrayBuffer | null>;
// ─────────────────────────────────────────────────────────────────────────
// Code Execution — see /threads/code-execution
// ─────────────────────────────────────────────────────────────────────────
runCode(source: string, options?: CodeExecutionOptions): CodeExecution;
// ─────────────────────────────────────────────────────────────────────────
// Execution State — see /threads/execution-state
// ─────────────────────────────────────────────────────────────────────────
execution: ExecutionState | null;
terminate(): Promise<void>;
// ─────────────────────────────────────────────────────────────────────────
// Runtime Context (Non-Portable)
// ─────────────────────────────────────────────────────────────────────────
readonly _notPackableRuntimeContext?: Record<string, unknown>;
}
/**
* A scheduled effect pending execution.
*/
interface ScheduledEffect {
id: string;
name: string;
args: Record<string, unknown>;
scheduledAt: number; // microseconds
createdAt: number; // microseconds
}