Graph control
In shortReference for graph-control methods on the TypeScript client: ping, graph, graphTools, observability, graphStateSchema, stopGraph, fixGraph, and setup.
- 7 min read
- 10 sections
- Updated
- v0.9.2
- Markdown
Seven methods on AgentFlowClient describe or control the graph itself rather than a single conversation: what the graph is, what tools it has, what its state looks like, what a run did, and how to stop or repair one.
Source: src/client.ts, src/endpoints/
| Method | Endpoint | Returns |
|---|---|---|
ping() |
GET /ping |
Promise<PingResponse> |
graph() |
GET /v1/graph |
Promise<GraphResponse> |
graphTools() |
GET /v1/graph/tools |
Promise<GraphToolsResponse> |
observability(threadId, runId?) |
GET /v1/observability/{thread_id} |
Promise<ObservabilityResponse> |
graphStateSchema() |
GET /v1/graph:StateSchema |
Promise<StateSchemaResponse> |
stopGraph(threadId, config?) |
POST /v1/graph/stop |
Promise<StopGraphResponse> |
fixGraph(threadId, config?) |
POST /v1/graph/fix |
Promise<FixGraphResponse> |
Every response follows the same envelope: the payload under data, and { request_id, timestamp, message } under metadata.
ping()
ping(): Promise<PingResponse>Liveness check. Use it to fail fast at startup instead of discovering a bad baseUrl on the first invoke().
interface PingResponse {
data: string; // the server's pong payload
metadata: ResponseMetadata;
}try {
await client.ping();
} catch {
throw new Error(`10xGraph API not reachable at ${baseUrl}`);
}ping() does not require the graph to be healthy — it only proves the HTTP server is answering. Use graph() when you need to know the graph itself loaded.
graph()
graph(): Promise<GraphResponse>Returns the graph’s topology and the server capabilities attached to it.
interface GraphResponse {
data: {
info: {
node_count: number;
edge_count: number;
checkpointer: boolean;
checkpointer_type: string;
publisher: boolean;
store: boolean;
interrupt_before: string[];
interrupt_after: string[];
context_type: string;
id_generator: string;
id_type: string;
state_type: string;
state_fields: string[];
};
nodes: Array<{ id: string; name: string }>;
edges: Array<{ id: string; source: string; target: string }>;
};
metadata: ResponseMetadata;
}info is the honest answer to “what can this deployment do”:
| Field | Why it matters |
|---|---|
checkpointer / checkpointer_type |
false means config.thread_id will not persist anything between calls. |
store |
false means the memory methods have no backend. |
interrupt_before / interrupt_after |
Node names where the graph pauses for human input. See AgentState.execution_meta.interrupt below. |
id_type / id_generator |
How thread and message ids are minted on this server. |
state_fields |
The field names of the graph’s state type; graphStateSchema() returns their full schema. |
The server also returns info.is_realtime, which is true when the graph is rooted at a live agent and therefore accepts realtime() over WS /v1/graph/live rather than invoke/stream/wsStream. It is not on the GraphInfo TypeScript interface yet, so read it with a cast:
const { info } = (await client.graph()).data;
const liveCapable = Boolean((info as { is_realtime?: boolean }).is_realtime);graphTools()
graphTools(): Promise<GraphToolsResponse>Lists every tool exposed by the graph’s tool nodes, grouped by node, each tagged with where it came from.
type ToolSource = 'local' | 'mcp' | 'remote';
interface GraphTool {
name: string;
description: string;
source: ToolSource;
parameters: Record<string, any>; // JSON Schema, OpenAI function-calling shape
}
interface GraphToolNode {
node_name: string;
tool_count: number;
tools: GraphTool[];
}
interface GraphToolsResponse {
data: {
node_count: number;
tool_count: number;
nodes: GraphToolNode[];
};
metadata: ResponseMetadata;
}source |
Origin |
|---|---|
local |
A Python function registered on the tool node. |
mcp |
Discovered from an MCP server attached to the node. |
remote |
Declared in server 10xgraph.json, executed by a matching client handler. |
A graph with no tool nodes returns nodes: [] and tool_count: 0. That is a valid graph, not an error.
See how-to/client/graph-utilities for worked examples, including verifying that your remote tools registered.
observability(threadId, runId?)
observability(threadId: string, runId?: string): Promise<ObservabilityResponse>Returns the reconstructed trace for one run of a thread: a span tree, an event list, and aggregated token usage. Omitting runId returns the most recent run.
interface ObservabilityResponse {
data: {
thread_id: string;
run_count: number;
run_ids: string[]; // oldest first
run: ObsRun | null; // null when nothing has been recorded
};
metadata: ResponseMetadata;
}
interface ObsRun {
run_id: string;
thread_id: string;
status: string;
started_at: number | null; // UNIX seconds
finished_at: number | null;
duration_ms: number;
spans: ObsSpan[];
events: ObsEvent[];
usage: ObsTokenUsage;
llm_calls: number;
tool_calls: number;
iterations: number;
}
interface ObsSpan {
id: string;
name: string;
kind: 'root' | 'node' | 'llm' | 'tool';
parent: string | null; // span id; null for the root
start_ms: number; // offset from the run start
duration_ms: number;
model?: string | null; // set on 'llm' spans
input_tokens?: number | null;
output_tokens?: number | null;
}
interface ObsEvent {
id: string;
type: string;
node: string;
offset_ms: number; // offset from the run start
summary: string;
}
interface ObsTokenUsage {
prompt_tokens: number;
completion_tokens: number;
reasoning_tokens: number;
total_tokens: number;
}threadId is typed string here, unlike most thread methods which accept string | number.
Spans nest root → node → llm | tool via parent. start_ms and offset_ms are relative to the start of the run, so add started_at * 1000 to place them on a wall clock.
A server with telemetry recording disabled returns run_count: 0 and run: null instead of raising.
graphStateSchema()
graphStateSchema(): Promise<StateSchemaResponse>Returns the JSON Schema of the graph’s state type. Use it to build dynamic forms, validate an initial_state before invoking, or generate types.
interface StateSchemaResponse {
data: {
title?: string;
description?: string;
type?: string;
properties: Record<string, FieldSchema>;
required?: string[];
$defs?: Record<string, any>;
[key: string]: any;
};
metadata: ResponseMetadata;
}
interface FieldSchema {
type?: string | string[];
description?: string;
default?: any;
items?: any;
properties?: Record<string, FieldSchema>;
required?: string[];
enum?: any[];
$ref?: string;
$defs?: Record<string, any>;
anyOf?: any[];
allOf?: any[];
oneOf?: any[];
[key: string]: any;
}This is the schema of the state type the graph is compiled with, which may be a subclass of AgentState carrying your own fields.
stopGraph(threadId, config?)
stopGraph(threadId: string, config?: Record<string, any>): Promise<StopGraphResponse>Sets a stop flag on the thread. The graph checks it between nodes and halts before starting the next one.
interface StopGraphResponse {
data: {
success: boolean;
message: string;
thread_id: string;
stopped_at?: string; // ISO timestamp
};
metadata: ResponseMetadata;
}It is a request, not a guarantee: the currently executing node runs to completion, so one more message may still arrive. Thread state is preserved, so the next call on the same thread_id resumes from where the flag was checked. Stopping a thread that is not running is not an error — success can still be true.
Breaking out of a for await loop over stream() closes your end of the connection but does not stop the graph. Call stopGraph() as well.
fixGraph(threadId, config?)
fixGraph(threadId: string, config?: Record<string, any>): Promise<FixGraphResponse>Repairs a thread whose history contains tool calls with no matching results, which happens when a run is cut off mid-tool-call. The LLM sees the orphaned calls and believes it is still waiting, so the thread appears stuck.
interface FixGraphResponse {
data: {
success: boolean;
message: string;
removed_count: number;
state?: Record<string, any>; // state after the repair, when available
};
metadata: ResponseMetadata;
}removed_count: 0 means the thread was already valid.
AgentState
AgentState is the state container the graph carries between nodes. It is returned by invoke() (as result.state), by stream() on state chunks, and by threadState().
class AgentState {
context: Message[];
context_summary: string | null;
execution_meta: ExecutionMeta;
}
interface ExecutionMeta {
current_node: string;
step: number;
interrupt?: {
node: string;
reason: string;
status: string;
data?: Record<string, any>;
};
is_running: boolean;
is_interrupted: boolean;
is_stopped_requested: boolean;
}| Field | Description |
|---|---|
context |
The conversation as the graph sees it: every Message currently in the working window. |
context_summary |
A rolling summary of messages trimmed out of context, or null when nothing has been summarized. |
execution_meta.current_node |
Node the graph is at. Starts at 'START'. |
execution_meta.step |
Steps executed in this run. Compare against recursion_limit. |
execution_meta.interrupt |
Present only when the graph paused. This is how a client detects a human-in-the-loop pause. |
execution_meta.is_running |
true while the run is in flight. |
execution_meta.is_interrupted |
true when the graph is paused at an interrupt point. |
execution_meta.is_stopped_requested |
true after stopGraph() has been accepted but before the graph has halted. |
The constructor takes a partial object and Object.assigns it, so a graph compiled with a custom state type carries its extra fields through at runtime even though they are not on the TypeScript class. Read graphStateSchema() for the authoritative field list of a given deployment.
Detecting a human-in-the-loop pause
An interrupt is not an error. The run ends normally and the state comes back paused:
const result = await client.invoke([userMessage], {
config: { thread_id: 'approval-1' },
});
const interrupt = result.state?.execution_meta.interrupt;
if (interrupt) {
// Show the approval UI. `data` carries whatever the node attached.
const approved = await askUser(interrupt.reason, interrupt.data);
// Resume by invoking the same thread again.
await client.invoke([Message.text_message(approved ? 'approved' : 'rejected')], {
config: { thread_id: 'approval-1' },
});
}graph().data.info.interrupt_before and interrupt_after tell you up front which nodes can pause, so a UI can be built for them before the first pause happens.
What you learned
ping()proves the server answers;graph()proves the graph loaded and reports what the deployment supports.graphTools()shows what the model can actually call, taggedlocal,mcp, orremote.observability()returns one run at a time as a span tree plus token usage;run_idslists the rest.stopGraph()is cooperative andfixGraph()repairs orphaned tool calls.setup()transmits the schemas of your client-side tools; handlers never leave your process.AgentState.execution_meta.interruptis how a client detects that the graph is waiting on a human.
Next step
See how-to/client/graph-utilities for task-oriented recipes, or Register remote tools for registering the client-side tools that setup() transmits.