Ask the user for input
Pause mid-turn, collect typed input as a form, and resume with the answer
An agent can stop mid-turn, ask the user a typed question, and continue once it has the answer. Astropods renders the question as a form in the chat surface and hands the response back to your code as data, not prose.
MCP calls this elicitation. Frameworks call it human-in-the-loop. The platform treats it as one primitive, so the same call works whether you are confirming a destructive action or collecting a value the agent could not infer.
When to use it
Ask for input when the agent cannot proceed correctly on its own judgment:
- Confirm before an irreversible action. Deleting records, posting to a shared tracker, spending money.
- Collect a value you cannot infer. A target environment, a date range, a recipient.
- Offer a choice. Several valid options, one decision.
A read-only tool does not need to ask.
Confirmations are also a defense against prompt injection. An agent that reads issue text, web pages, or user-supplied documents is reading content it does not control. Putting a person between that content and an irreversible action means injected instructions cannot act on their own.
Never collect passwords, API keys, or other secrets this way. The response is stored with the conversation and may appear in logs and traces. Use secrets instead.
Ask for input
Your adapter’s stream() receives a StreamOptions
object, and the bridge puts two methods on it:
render() is the primitive. elicit() calls it with a positional signature
shaped like MCP’s elicitation, and is the one most agents want. Both are
optional, so check before calling: a caller that drives the gRPC stream itself
receives neither.
Either way the promise resolves with the user’s response, or rejects if the
turn is stopped or superseded, so await it or attach a .catch. If the user’s
surface cannot render a form and you did not allow a prose reply, the promise
rejects with UnsupportedRenderableError rather than coercing the question to
text. Audio surfaces never render forms.
render()
One object describes the whole request:
elicit()
The same call, with message and dataSchema as positional arguments and the
rest in a third options argument. It accepts every render() field except
kind, and it offers submit, decline, and cancel rather than submit and
cancel:
The optional fields go in the third argument: value, allowedActions,
intent, and id. Here value proposes an answer the user can edit, and
intent labels the request as a tool permission:
How a schema becomes a form
The dataSchema in either call is the form. It must be an object schema: its
properties become the fields, in the order you declare them, and a schema
with no properties renders no fields at all.
Each property’s shape picks its control:
Other keywords fill in the rest of the field:
So the elicit() call above renders two fields: Environment, a required
select of staging and production, and Notes, an optional text area.
Neither carries a title, so each label comes from humanizing its property
name.
A prefilled value seeds the form. Fields it does not mention start empty: a
multi-select as [], a checkbox as unchecked, everything else blank.
Name the specifics in message: name the record, not just its id, so the
person is answering about the thing they think they are.
What the user can do
Treat anything other than submit as a decline. Cancel is always available, so a user can never be trapped by a pending question.
Behavior worth designing for
A pending question never expires. It waits as long as the thread is open, gating only that thread. A user who walks away and answers tomorrow gets the same result.
Re-check before you write. State you read before asking may be stale by the time the answer arrives. Between the question and the answer, the record may already be archived.
Say what you are about to do, not what you did. What you stream while waiting is not the result of the action.
A restart may abandon an in-flight question. The answer is still recorded, but an agent holding the await in memory loses it. Frameworks that checkpoint their runs, such as Mastra, resume after a restart.
Framework support
Next steps
- Ask for input from a Mastra tool: the suspend and resume path, with durable storage
- Build your own adapter: the
StreamOptionsyour adapter receives - Secrets: the right way to handle credentials
- Serve a frontend from your agent: render your own UI instead of using the chat surface