feat: add workflow query endpoint for agent state inspection#173
Open
feat: add workflow query endpoint for agent state inspection#173
Conversation
…al workflow queries Expose Temporal workflow queries via the tasks REST API. The endpoint delegates to the existing TemporalAdapter.query_workflow method and relies on the global exception handler for error mapping: - TemporalWorkflowNotFoundError → 404 - TemporalQueryError → 400 (changed from 500 to reflect client error) - Other TemporalError subclasses → 500 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gabrycina
added a commit
to scaleapi/scale-agentex-python
that referenced
this pull request
Mar 24, 2026
Adds a default @workflow.query handler named "get_current_state"
to BaseWorkflow. Returns a _workflow_state string that defaults
to "initialized".
Agents using StateMachine should override this to return the
state machine's current state, enabling external callers to
detect turn completion (state == "waiting_for_input").
Example override:
@workflow.query(name="get_current_state")
def get_current_state(self) -> str:
return self.state_machine.get_current_state()
Companion to scaleapi/scale-agentex#173 which adds
GET /tasks/{task_id}/query/{query_name} endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gabrycina
added a commit
to scaleapi/scale-agentex-python
that referenced
this pull request
Mar 24, 2026
Adds a default @workflow.query handler named "get_current_state"
to BaseWorkflow. Returns a _workflow_state string that defaults
to "initialized".
Agents using StateMachine should override this to return the
state machine's current state, enabling external callers to
detect turn completion (state == "waiting_for_input").
Example override:
@workflow.query(name="get_current_state")
def get_current_state(self) -> str:
return self.state_machine.get_current_state()
Companion to scaleapi/scale-agentex#173 which adds
GET /tasks/{task_id}/query/{query_name} endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile correctly flagged that TemporalQueryError is used as a catch-all in the adapter, not just for missing query handlers. Keeping it as 500 (ServiceError) is correct since connectivity, serialization, and other server-side failures also raise this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gabrycina
added a commit
to scaleapi/scale-agentex-python
that referenced
this pull request
Mar 24, 2026
Adds a default @workflow.query handler named "get_current_state" to BaseWorkflow. Automatically returns the state machine's current state if self.state_machine exists (covers all StateMachine-based agents without any override needed). Falls back to _workflow_state string for non-state-machine agents. Enables external callers to detect turn completion: "waiting_for_input" = agent is done, ready for next message "researching" = agent is still working Companion to scaleapi/scale-agentex#173 which adds GET /tasks/{task_id}/query/{query_name} endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gabrycina
added a commit
to scaleapi/scale-agentex-python
that referenced
this pull request
Mar 24, 2026
Adds a default @workflow.query handler named "get_current_state"
to BaseWorkflow. Returns "unknown" by default — agents override
to report their actual state.
Example for StateMachine-based agents:
@workflow.query(name="get_current_state")
def get_current_state(self) -> str:
return self.state_machine.get_current_state()
Companion to scaleapi/scale-agentex#173 which adds
GET /tasks/{task_id}/query/{query_name} endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
handle.query(name, None) passes None as a positional argument to the query handler, causing "takes 1 positional argument but 2 were given" for handlers that take no args. Only pass arg when it's not None. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
||
|
|
||
| @router.get( | ||
| "/{task_id}/query/{query_name}", |
Collaborator
There was a problem hiding this comment.
Can we add tests for this new route?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
GET /tasks/{task_id}/query/{query_name}endpoint that proxies Temporal workflow queries through the Agentex REST API.Problem
When programmatically invoking agentic (Temporal) agents, callers have no way to know when the agent has finished processing a turn. The task status stays
RUNNINGthroughout. Agents using the state machine SDK internally track their state (waiting_for_input,researching, etc.) but this isn't exposed externally.Solution
Expose Temporal's built-in workflow query API through the existing REST API. Agents that register
@workflow.queryhandlers can now be queried for their current state without affecting execution.New endpoint
Changes
tasks.pyusing existingDTemporalAdapterdependencyTemporalQueryErrorfrom 500 to 400 (invalid query is a client error)Companion change needed
Agents need to register
@workflow.queryhandlers to be queryable. The state machine SDK should add a defaultget_current_statequery handler — tracked separately in the SDK repo.Use case
The Agent Plane communication service (meta-registry-comms-svc) invokes agents via RPC and polls for responses. With this endpoint, it can check
get_current_stateto detect when the agent transitions back towaiting_for_input, providing a reliable turn-completion signal for multi-turn conversations.Follows the same pattern as Google A2A protocol's
INPUT_REQUIREDtask state.🤖 Generated with Claude Code
Greptile Summary
This PR exposes Temporal's workflow query API through a new
GET /tasks/{task_id}/query/{query_name}REST endpoint, enabling callers to inspect internal agent state (e.g.waiting_for_input) without polling task status. A small guard is also added inquery_workflowto avoid passingNoneas a positional argument tohandle.query()when no arg is needed.Key observations:
DAuthorizedIdwithreadpermission) is correctly applied to the new endpoint.arg is not Noneguard inadapter_temporal.pyis the right fix — passingNonetohandle.query()would serialize it and send it to the query handler as an actual argument.TemporalQueryErrorremains aServiceErrorwithcode = 500inexceptions.py; the PR description claims this was changed to 400, but the file was not modified.DTemporalAdapterdirectly rather than going through a domain use-case, which breaks the consistent layered pattern followed by every other route intasks.pyand skips the standard DB-level task existence check.Confidence Score: 4/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant C as Caller participant R as tasks.py route participant A as TemporalAdapter participant T as Temporal Server C->>R: GET /tasks/{task_id}/query/{query_name} R->>R: DAuthorizedId — check read permission on task_id R->>A: query_workflow(workflow_id=task_id, query=query_name) A->>A: check client connected A->>T: get_workflow_handle(task_id) alt arg is not None A->>T: handle.query(query_name, arg) else no arg A->>T: handle.query(query_name) end alt success T-->>A: query result A-->>R: result R-->>C: {"task_id": "...", "query": "...", "result": "..."} else workflow not found T-->>A: not found error A-->>R: TemporalWorkflowNotFoundError (404) R-->>C: 404 Not Found else query/other error T-->>A: exception A-->>R: TemporalQueryError (500) R-->>C: 500 Internal Server Error endPrompt To Fix All With AI
Reviews (3): Last reviewed commit: "fix: don't pass None arg to Temporal que..." | Re-trigger Greptile