iflytek/astron-agent · error · CustomException
QUESTION_ANSWER_NODE_EXECUTION_ERROR
QUESTION_ANSWER_NODE_EXECUTION_ERROR
Error message
No matching option and no default option
What it means
The question-answer node resumed from an interrupt with the user's reply and tried to match it against the node's configured static options. No option whose `name` equals the reply was found in `processed_options`, and none of the configured options was marked with `OptionType.DEFAULT`, so the node has no output to produce and aborts with QUESTION_ANSWER_NODE_EXECUTION_ERROR.
Solutions
- Make the resume payload send the exact option `name` string as configured in the question-answer node (or the value the frontend received in the interrupt event).
- Configure one option with type DEFAULT in the question-answer node so unmatched replies fall back to it.
- Normalize the reply before resuming (trim/case-fold) on the client, or loosen the comparison in handle_static_option_response.
- Verify the workflow's current options match those the interrupting session presented; re-open a new session if the node config changed mid-conversation.
Example fix
// before: resuming with whatever the user typed
await resume_workflow(event_id, {"content": user_text})
// after: map free text back to a configured option name, or fall back to the default option
matched = next((o for o in options if o["name"].strip().lower() == user_text.strip().lower()), None)
await resume_workflow(event_id, {"content": matched["name"] if matched else default_option_name}) Defensive patterns
Strategy: validation
Validate before calling
def can_resume(options, reply: str) -> bool:
return any(o["name"] == reply or o.get("type") == "DEFAULT" for o in options)
if not can_resume(configured_options, user_reply):
raise ValueError(f"reply '{user_reply}' matches no option and no default is configured") Type guard
def is_valid_option(options: list[dict], reply: str) -> dict | None:
return next((o for o in options if o["name"] == reply or o.get("type") == "DEFAULT"), None) Try / catch
try:
result = await resume_workflow(event_id, {"content": user_reply})
except CustomException as e:
if "No matching option" in str(e.err_msg):
result = await resume_workflow(event_id, {"content": default_option_name})
else:
raise Prevention
- Always configure a DEFAULT option on question-answer nodes used with free-text clients.
- Send the exact option name from the interrupt event payload back on resume, never user-typed text.
- Normalize (trim/case-fold) option names on both client and workflow config.
- Invalidate/restart sessions when the node's option configuration changes.
When it happens
Trigger: Calling the workflow resume path (async_execute -> handle_static_option_response) where `resume_data.content` (e.g. "A") does not exactly equal any configured option's `name` (case/whitespace-sensitive string comparison) and no option has `type == OptionType.DEFAULT.value`.
Common situations: Frontend sends a free-text answer or a different label/ID than the exact option name configured in the workflow editor; options were renamed in the workflow after a chat session was interrupted; an old client caches stale option labels; option names contain differing whitespace or casing; developer forgot to designate a default option as a catch-all.
Understand the failure class
Background: "Must pass :limit option" / "Missing required option" — required option errors explained — this error's family across 41 libraries.
Related errors
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/ea1f4d788a10716f.
Report an issue: GitHub.
Appendix: source
Thrown at core/workflow/engine/nodes/question_answer/question_answer_node.py:416
:param inputs: Input data dictionary
:param outputs: Output data dictionary
:param resume_data: Resume data object containing user reply content
:param variable_pool: Variable pool for getting variable values
:return: NodeRunResult object containing node execution result and related information
"""
user_reply = resume_data.content # User reply content, e.g., "A"
await span_context.add_info_events_async({"option_reply_content": user_reply})
option_output: Option | None = None
# Single traversal to find matching item and default item
for option in self.processed_options:
# Standardized option name comparison
if option.name == user_reply or option.type == OptionType.DEFAULT.value:
option_output = option
break
if not option_output:
raise CustomException(
err_code=CodeEnum.QUESTION_ANSWER_NODE_EXECUTION_ERROR,
err_msg="No matching option and no default option",
)
if option_output:
outputs.update(option_output.dict())
# The ID to output for option answer is actually the option's name
outputs[SystemOutputVariable.ID.value] = outputs["name"]
await span_context.add_info_events_async(
{"static_option_response": json.dumps(outputs, ensure_ascii=False)}
)
order_outputs = {}
for output in self.output_identifier:
if output in outputs:
order_outputs.update({output: outputs.get(output)})View on GitHub (pinned to 5e758547a8)