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

  1. 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).
  2. Configure one option with type DEFAULT in the question-answer node so unmatched replies fall back to it.
  3. Normalize the reply before resuming (trim/case-fold) on the client, or loosen the comparison in handle_static_option_response.
  4. 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

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)