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.

MethodRouteDescription
GET/api/v1/workflows/{id}/adk/{nodeId}/queries/get_latest_orchestration_resultLatest 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_eventsComplete 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_detailsPending 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_confirmationWhether the workflow is currently paused waiting for confirmation.
GET/api/v1/workflows/{id}/adk/{nodeId}/queries/get_latest_invocation_traceExecution 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.

MethodRouteDescription
POST/api/v1/workflows/{id}/adk/{nodeId}/signals/send_messageSend 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_conversationRequest the workflow to stop. The main orchestration loop exits after the current turn completes.
POST/api/v1/workflows/{id}/adk/{nodeId}/signals/confirm_actionRespond 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

  1. Your LLM service returns a hitlRequest in its response, containing a prompt and optional actionDetails.
  2. The ADK worker records the request and pauses the orchestration loop.
  3. The workflow enters a waiting state. The is_waiting_for_confirmation query returns true.
  4. An external system (your UI, a Slack bot, an approval service) calls get_pending_confirmation_details to retrieve the request.
  5. The external system calls the confirm_action signal endpoint with requestId and approved (boolean).
  6. The workflow resumes. If approved, a system message confirms the action. If denied, a system message informs the agent.
sequenceDiagram participant LLM as Your LLM Service participant ADK as ADK Worker participant API as Graph Compose API participant UI as Your Application LLM->>ADK: Response with hitlRequest ADK->>ADK: Record pending confirmation ADK->>ADK: Pause orchestration loop UI->>API: GET /workflows/{id}/adk/{nodeId}/queries/is_waiting_for_confirmation API-->>UI: true UI->>API: GET /workflows/{id}/adk/{nodeId}/queries/get_pending_confirmation_details API-->>UI: { request_id, prompt, action_details } UI->>API: POST /workflows/{id}/adk/{nodeId}/signals/confirm_action API->>ADK: Forward signal ADK->>ADK: Resume orchestration ADK->>LLM: Next turn with confirmation result

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:

FieldTypeDescription
latestOrchestrationResultobject | undefinedFinal response text, session state, exit_flow, and wait_for_user_input flags.
conversationHistoryarrayAll conversation events with agent attribution, timestamps, and content (text, function calls, function responses, HITL metadata).
isStoppedbooleanWhether the workflow has been stopped via the end_conversation signal.
pendingConfirmationobjectContains isWaiting (boolean) and details (the pending confirmation object or null).

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.

Next steps