You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two of the .NET Azure Functions host's built-in workflow HTTP endpoints return text/plain where callers need machine-parsable JSON:
The asynchronous workflow-start 202 returns an English sentence with the run ID embedded in it, ignoring the Accept header entirely.
The status and respond endpoints always return JSON on success, but fall back to text/plain on errors when the client sends no Accept header.
Both are pre-existing on main. Neither was introduced by #48, but the Functions-native client API added there makes the start endpoint's response shape more visible.
This was split out of #51, which lists "Plain-text versus JSON asynchronous-start responses" under Additional coherence questions and explicitly scopes it out: "These are broader than casing and should not be changed implicitly, but they affect the scope of a fully coherent public contract."#51 stays a casing/naming issue; this one covers content type and parsability.
1. Asynchronous start returns prose
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs:91-93, in RunWorkflowOrchestrationHttpTriggerAsync:
HttpResponseDataresponse=req.CreateResponse(HttpStatusCode.Accepted);awaitresponse.WriteStringAsync($"Workflow orchestration started for {workflowName}. Orchestration runId: {resolvedInstanceId}");returnresponse;
POST /api/workflows/{name}/run returns:
Workflow orchestration started for CancelOrder. Orchestration runId: 7f3e2a1b...
The run ID is the only machine-consumable value in the response, and it is the value the caller needs to poll workflows/{name}/status/{runId}. Extracting it requires substring-parsing an English sentence whose wording is not part of any documented contract.
This is the only response in BuiltInFunctions.cs that does not consult AcceptsJson(req). Sending Accept: application/json still returns prose. Nearby code already negotiates correctly:
WaitForWorkflowCompletionAsync (line 514) — the wait-for-response branch of the same function
CreateAcceptedResponseAsync (line 631) — the agent equivalent, which also surfaces the session ID in a response header
Language parity
The Python host already returns JSON from the equivalent endpoint (python/packages/azurefunctions/agent_framework_azurefunctions/_app.py:515-526):
Python's integration tests assert on data["statusQueryGetUri"] and follow it directly. The .NET host offers no equivalent affordance.
2. JSON-only endpoints emit text/plain on errors
CreateErrorResponseAsync (line 569) defaults to text/plain when the request carries no Accept header:
if(acceptsJson??AcceptsJson(req))
AcceptsJson requires an explicit Accept header naming application/json; many clients send none. Two endpoints always write JSON on success but route failures through this helper:
Endpoint
Success
Error (no Accept header)
GET workflows/{name}/status/{runId}
JSON (line 126)
text/plain (lines 108, 114)
POST workflows/{name}/respond/...
JSON (line 200)
text/plain (lines 147, 157, 163, 170, 177, 189)
A client calling response.json() succeeds on 200/202 and throws on 400/404. The response shape flips within a single endpoint, and it flips precisely when the caller is trying to determine what went wrong.
RunAgentHttpAsync and WaitForWorkflowCompletionAsync are not affected — they negotiate consistently across success and failure.
Proposed direction
For the asynchronous start response, emit a JSON body carrying at minimum the run ID and the workflow name. Worth deciding as part of this issue:
Whether to include a status polling URL (statusQueryGetUri) as Python does. Note the .NET status route is only registered when the workflow opts in via AddWorkflow(enableStatusEndpoint: true), so the field would have to be conditional or the opt-in reconsidered.
Whether to switch outright or content-negotiate on Accept, keeping the current string for text clients. Content negotiation matches the agent path and the wait-for-response path, and avoids breaking callers that parse the current sentence today.
Field names should be settled against Consider unifying public HTTP API surfaces on camelCase #51 rather than independently, so this does not introduce a third naming convention. Python uses instanceId; .NET's status endpoint uses runId.
For the error responses, either pass acceptsJson: true at the five call sites belonging to JSON-only endpoints, or have those endpoints force JSON since they have no meaningful text representation.
Compatibility
Changing the 202 body is a breaking HTTP contract change for anyone parsing the current string. Content negotiation avoids that; an outright switch does not and would need a breaking-change label plus a CHANGELOG entry.
The error-response fix is lower risk. It only affects requests that send no Accept header, and it moves them from an undocumented plain-text body to the ErrorResponse shape those endpoints already return to JSON clients.
Completion criteria
Asynchronous workflow start returns a machine-parsable body, with the compatibility strategy documented.
JSON-only endpoints return JSON on both success and failure.
Summary
Two of the .NET Azure Functions host's built-in workflow HTTP endpoints return
text/plainwhere callers need machine-parsable JSON:202returns an English sentence with the run ID embedded in it, ignoring theAcceptheader entirely.text/plainon errors when the client sends noAcceptheader.Both are pre-existing on
main. Neither was introduced by #48, but the Functions-native client API added there makes the start endpoint's response shape more visible.This was split out of #51, which lists "Plain-text versus JSON asynchronous-start responses" under Additional coherence questions and explicitly scopes it out: "These are broader than casing and should not be changed implicitly, but they affect the scope of a fully coherent public contract." #51 stays a casing/naming issue; this one covers content type and parsability.
1. Asynchronous start returns prose
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs:91-93, inRunWorkflowOrchestrationHttpTriggerAsync:POST /api/workflows/{name}/runreturns:The run ID is the only machine-consumable value in the response, and it is the value the caller needs to poll
workflows/{name}/status/{runId}. Extracting it requires substring-parsing an English sentence whose wording is not part of any documented contract.This is the only response in
BuiltInFunctions.csthat does not consultAcceptsJson(req). SendingAccept: application/jsonstill returns prose. Nearby code already negotiates correctly:WaitForWorkflowCompletionAsync(line 514) — the wait-for-response branch of the same functionCreateAcceptedResponseAsync(line 631) — the agent equivalent, which also surfaces the session ID in a response headerLanguage parity
The Python host already returns JSON from the equivalent endpoint (
python/packages/azurefunctions/agent_framework_azurefunctions/_app.py:515-526):Python's integration tests assert on
data["statusQueryGetUri"]and follow it directly. The .NET host offers no equivalent affordance.2. JSON-only endpoints emit text/plain on errors
CreateErrorResponseAsync(line 569) defaults totext/plainwhen the request carries noAcceptheader:AcceptsJsonrequires an explicitAcceptheader namingapplication/json; many clients send none. Two endpoints always write JSON on success but route failures through this helper:Acceptheader)GET workflows/{name}/status/{runId}text/plain(lines 108, 114)POST workflows/{name}/respond/...text/plain(lines 147, 157, 163, 170, 177, 189)A client calling
response.json()succeeds on200/202and throws on400/404. The response shape flips within a single endpoint, and it flips precisely when the caller is trying to determine what went wrong.RunAgentHttpAsyncandWaitForWorkflowCompletionAsyncare not affected — they negotiate consistently across success and failure.Proposed direction
For the asynchronous start response, emit a JSON body carrying at minimum the run ID and the workflow name. Worth deciding as part of this issue:
statusQueryGetUri) as Python does. Note the .NET status route is only registered when the workflow opts in viaAddWorkflow(enableStatusEndpoint: true), so the field would have to be conditional or the opt-in reconsidered.Accept, keeping the current string for text clients. Content negotiation matches the agent path and the wait-for-response path, and avoids breaking callers that parse the current sentence today.instanceId; .NET's status endpoint usesrunId.For the error responses, either pass
acceptsJson: trueat the five call sites belonging to JSON-only endpoints, or have those endpoints force JSON since they have no meaningful text representation.Compatibility
Changing the
202body is a breaking HTTP contract change for anyone parsing the current string. Content negotiation avoids that; an outright switch does not and would need abreaking-changelabel plus a CHANGELOG entry.The error-response fix is lower risk. It only affects requests that send no
Acceptheader, and it moves them from an undocumented plain-text body to theErrorResponseshape those endpoints already return to JSON clients.Completion criteria
Acceptheader.demo.httpand the sample READMEs are updated to show the new responses.