Monitoring and Signals
ADK workflows run as child workflows with their own lifecycle and state. The Graph Compose API exposes query and signal endpoints that let you read conversation history, monitor progress, and control execution from your application.
Querying workflow state
Query endpoints return a snapshot of the ADK child workflow's current state. All routes require authentication via API key or session token, and use the parent workflow ID and ADK node ID to identify the child workflow.
| Method | Route | Description |
|---|---|---|
GET | /api/v1/workflows/{id}/adk/{nodeId}/queries/get_latest_orchestration_result | Latest agent turn result: final response text, session state, cycle count, and whether the workflow hit the global iteration limit. |
GET | /api/v1/workflows/{id}/adk/{nodeId}/queries/get_session_events | Complete conversation history: all messages, tool calls, function responses, and HITL events with timestamps and agent attribution. |
GET | /api/v1/workflows/{id}/adk/{nodeId}/queries/get_pending_confirmation_details | Pending HITL confirmation request details: request ID, tool calls, prompt, action details, and confirmation state. Returns null if no confirmation is pending. |
GET | /api/v1/workflows/{id}/adk/{nodeId}/queries/is_waiting_for_confirmation | Whether the workflow is currently paused waiting for confirmation. |
GET | /api/v1/workflows/{id}/adk/{nodeId}/queries/get_latest_invocation_trace | Execution trace including orchestration result, workflow metadata (cycle count, max cycles, termination status), pending confirmations, and orchestrator info. |
Query conversation history
const workflowId = 'wf-abc123'
const nodeId = 'support_agent'
const response = await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}/queries/get_session_events`,
{
headers: {
Authorization: 'Bearer YOUR_API_KEY',
},
},
)
const { data } = await response.json()
// data.queryResult contains the session events array
Sending signals
Signal endpoints send one-way messages to a running ADK workflow. They unblock waiting conditions or alter execution flow. The workflow processes signals on its next evaluation cycle, not immediately upon receipt.
| Method | Route | Description |
|---|---|---|
POST | /api/v1/workflows/{id}/adk/{nodeId}/signals/send_message | Send a new user message. The message is added to the conversation and the agent processes it on its next turn. |
POST | /api/v1/workflows/{id}/adk/{nodeId}/signals/end_conversation | Request the workflow to stop. The main orchestration loop exits after the current turn completes. |
POST | /api/v1/workflows/{id}/adk/{nodeId}/signals/confirm_action | Respond to a HITL confirmation request with requestId and approved (boolean). |
const workflowId = 'wf-abc123'
const nodeId = 'support_agent'
await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}/signals/send_message`,
{
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'What is the weather in San Francisco?',
}),
},
)
Human-in-the-Loop (HITL)
HITL lets your LLM service pause execution and wait for human approval before proceeding. This is useful for high-stakes actions like financial transactions, data deletion, or external API calls that cannot be reversed.
How it works
- Your LLM service returns a
hitlRequestin its response, containing apromptand optionalactionDetails. - The ADK worker records the request and pauses the orchestration loop.
- The workflow enters a waiting state. The
is_waiting_for_confirmationquery returnstrue. - An external system (your UI, a Slack bot, an approval service) calls
get_pending_confirmation_detailsto retrieve the request. - The external system calls the
confirm_actionsignal endpoint withrequestIdandapproved(boolean). - The workflow resumes. If approved, a system message confirms the action. If denied, a system message informs the agent.
What your LLM service returns for HITL
Imagine a travel booking agent that searches for flights on behalf of the user. When it finds a match, it asks for confirmation before charging the card. Your LLM service returns a hitlRequest alongside its text response:
Booking agent requesting confirmation
{
"content": "I found a direct flight from SFO to JFK on March 15 for $299. Should I go ahead and book it?",
"hitlRequest": {
"prompt": "Book flight AA-1234 SFO to JFK on March 15 for $299?",
"actionDetails": {
"flight_id": "AA-1234",
"price": 299,
"route": "SFO to JFK",
"date": "2026-03-15",
"passenger": "Jason"
}
}
}
Querying and confirming
When a HITL request is active, call get_pending_confirmation_details to retrieve the request details:
Check for pending confirmation
const workflowId = 'wf-abc123'
const nodeId = 'support_agent'
const response = await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}/queries/get_pending_confirmation_details`,
{
headers: {
Authorization: 'Bearer YOUR_API_KEY',
},
},
)
const { data } = await response.json()
// data.queryResult contains the pending confirmation object
Pending confirmation response
{
"request_id": "hitl-req-abc123",
"tool_calls": [],
"prompt": "Book flight AA-1234 SFO to JFK on March 15 for $299?",
"action_details": {
"flight_id": "AA-1234",
"price": 299,
"route": "SFO to JFK",
"date": "2026-03-15",
"passenger": "Jason"
},
"confirmed": null,
"confirmed_at": null
}
The confirmed field is null while pending, true after approval, and false after denial.
To approve or deny the request, call the confirm_action signal endpoint:
Confirm a HITL request
await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}/signals/confirm_action`,
{
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId: 'hitl-req-abc123',
approved: true,
}),
},
)
Aggregated state endpoint
This endpoint combines the results of all five queries into a single response. Use it to get a full snapshot of the workflow in one request.
GET /api/v1/workflows/{id}/adk/{nodeId}
const response = await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}`,
{
headers: {
Authorization: 'Bearer YOUR_API_KEY',
},
},
)
const { data } = await response.json()
The executionState object contains the AdkWorkflowState:
| Field | Type | Description |
|---|---|---|
latestOrchestrationResult | object | undefined | Final response text, session state, exit_flow, and wait_for_user_input flags. |
conversationHistory | array | All conversation events with agent attribution, timestamps, and content (text, function calls, function responses, HITL metadata). |
isStopped | boolean | Whether the workflow has been stopped via the end_conversation signal. |
pendingConfirmation | object | Contains isWaiting (boolean) and details (the pending confirmation object or null). |
The aggregated state endpoint is the most efficient option for polling. It returns all workflow data in a single request, avoiding multiple round trips for individual queries.
Polling patterns
ADK workflows are asynchronous. Use polling to monitor progress and detect completion or HITL pauses.
Poll for workflow completion
async function pollWorkflowState(workflowId: string, nodeId: string) {
const maxAttempts = 30
const pollInterval = 2000
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}`,
{
headers: {
Authorization: `Bearer ${process.env.GRAPH_COMPOSE_TOKEN}`,
},
},
)
const { data } = await response.json()
const state = data.executionState
if (state?.isStopped || state?.latestOrchestrationResult?.exit_flow) {
return {
status: 'completed',
response: state.latestOrchestrationResult?.final_response_text,
sessionState: state.latestOrchestrationResult?.session_state,
}
}
if (state?.pendingConfirmation?.isWaiting) {
return {
status: 'pending_confirmation',
details: state.pendingConfirmation.details,
}
}
await new Promise((resolve) => setTimeout(resolve, pollInterval))
}
throw new Error('Workflow did not complete within timeout')
}
Poll conversation history for new events
async function monitorConversation(workflowId: string, nodeId: string) {
let lastEventCount = 0
const poll = async () => {
const response = await fetch(
`https://api.graphcompose.com/api/v1/workflows/${workflowId}/adk/${nodeId}/queries/get_session_events`,
{
headers: {
Authorization: `Bearer ${process.env.GRAPH_COMPOSE_TOKEN}`,
},
},
)
const { data } = await response.json()
const events = data.queryResult || []
if (events.length > lastEventCount) {
const newEvents = events.slice(lastEventCount)
for (const event of newEvents) {
const text = event.content?.parts?.[0]?.text
if (text) {
console.log(`[${event.role}]: ${text}`)
}
}
lastEventCount = events.length
}
}
const intervalId = setInterval(poll, 2000)
return () => clearInterval(intervalId)
}
Error handling
ADK query and signal endpoints return standard ApiResponse shapes. Check the success field to detect errors.
Workflow not found
{
"success": false,
"message": "Failed to query child workflow: Workflow not found",
"data": null
}
Invalid query name
{
"success": false,
"message": "Invalid ADK query name: bad_query. Must be one of: get_latest_orchestration_result, get_session_events, get_pending_confirmation_details, is_waiting_for_confirmation, get_latest_invocation_trace",
"data": null
}
Unauthorized
{
"success": false,
"message": "Workflow access denied",
"data": null
}
Common scenarios:
- Workflow not found. The ADK child workflow may not exist yet if the parent workflow has not reached the ADK node. Retry after a delay.
- Workflow already completed. Temporal does not support queries on terminated workflows. Graph Compose persists ADK state to a database before termination, so the state endpoint continues to work after completion.
- Signal on a completed workflow. Sending a signal to a terminated workflow returns an error. Check workflow status before sending signals.