{"record":{"id":"ea1f4d788a10716f","repo":"iflytek/astron-agent","slug":"question-answer-node-execution-error","errorCode":"QUESTION_ANSWER_NODE_EXECUTION_ERROR","errorMessage":"No matching option and no default option","messagePattern":"No matching option and no default option","errorType":"error_code","errorClass":"CustomException","httpStatus":null,"severity":"error","filePath":"core/workflow/engine/nodes/question_answer/question_answer_node.py","lineNumber":416,"sourceCode":"        :param inputs: Input data dictionary\n        :param outputs: Output data dictionary\n        :param resume_data: Resume data object containing user reply content\n        :param variable_pool: Variable pool for getting variable values\n        :return: NodeRunResult object containing node execution result and related information\n        \"\"\"\n        user_reply = resume_data.content  # User reply content, e.g., \"A\"\n        await span_context.add_info_events_async({\"option_reply_content\": user_reply})\n\n        option_output: Option | None = None\n        # Single traversal to find matching item and default item\n        for option in self.processed_options:\n            # Standardized option name comparison\n            if option.name == user_reply or option.type == OptionType.DEFAULT.value:\n                option_output = option\n                break\n\n        if not option_output:\n            raise CustomException(\n                err_code=CodeEnum.QUESTION_ANSWER_NODE_EXECUTION_ERROR,\n                err_msg=\"No matching option and no default option\",\n            )\n\n        if option_output:\n            outputs.update(option_output.dict())\n\n        # The ID to output for option answer is actually the option's name\n        outputs[SystemOutputVariable.ID.value] = outputs[\"name\"]\n\n        await span_context.add_info_events_async(\n            {\"static_option_response\": json.dumps(outputs, ensure_ascii=False)}\n        )\n\n        order_outputs = {}\n        for output in self.output_identifier:\n            if output in outputs:\n                order_outputs.update({output: outputs.get(output)})","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/workflow/engine/nodes/question_answer/question_answer_node.py#L398-L434","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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.","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."],"exampleFix":"// before: resuming with whatever the user typed\nawait resume_workflow(event_id, {\"content\": user_text})\n\n// after: map free text back to a configured option name, or fall back to the default option\nmatched = next((o for o in options if o[\"name\"].strip().lower() == user_text.strip().lower()), None)\nawait resume_workflow(event_id, {\"content\": matched[\"name\"] if matched else default_option_name})","handlingStrategy":"validation","validationCode":"def can_resume(options, reply: str) -> bool:\n    return any(o[\"name\"] == reply or o.get(\"type\") == \"DEFAULT\" for o in options)\n\nif not can_resume(configured_options, user_reply):\n    raise ValueError(f\"reply '{user_reply}' matches no option and no default is configured\")","typeGuard":"def is_valid_option(options: list[dict], reply: str) -> dict | None:\n    return next((o for o in options if o[\"name\"] == reply or o.get(\"type\") == \"DEFAULT\"), None)","tryCatchPattern":"try:\n    result = await resume_workflow(event_id, {\"content\": user_reply})\nexcept CustomException as e:\n    if \"No matching option\" in str(e.err_msg):\n        result = await resume_workflow(event_id, {\"content\": default_option_name})\n    else:\n        raise","preventionTips":["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."],"tags":["workflow","question-answer","option-matching","resume"],"backgroundTag":"missing-required-option","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-19T12:17:13.211Z"}