Asking the user from a command
Updated
A command script can stop, ask the user a question and continue with the answer: Yes/No, a choice of buttons, or a small form. A second run replays from the start unless you persist answers yourself. Short recipe: Ask from a command.
Two kinds of confirmation#
| Static confirmation | A question from code | |
|---|---|---|
| Where it is set | requiresConfirmation + confirmationMessage in the command's json |
context.Confirm / Choose / Ask in the script |
| When the user sees it | before the command starts | while the command runs, when the code decides to ask |
| Text | fixed | built from data ($"{shortage} pcs short. Post anyway?") |
| Answers | OK / Cancel | Yes / No, your own buttons, or input fields |
Use the static confirmation when every run needs the same "Are you sure?". Use a question from code when the question depends on what the command found.
The three calls#
// Yes/No. Returns true or false.
if (shortage > 0 && !context.Confirm("negative", $"{shortage} pcs short. Post with a negative stock?"))
return;
// Your own buttons. Returns the caption of the button the user pressed.
var how = context.Choose("split", "Split the order?", "By warehouse", "By date", "Keep");
// A small form. Returns the entered values.
var form = context.Ask("move", "Move the remainder",
CommandQuestionField.Decimal("Qty", "Quantity", required: true, defaultValue: shortage),
CommandQuestionField.Date("OnDate", "Date"),
CommandQuestionField.Reference("Store", "Warehouse", "Store"));
var qty = form.Get<decimal>("Qty");
var onDate = form.Get<DateTime?>("OnDate");
var store = form.Get<Guid?>("Store");
The first argument is the question key. It must be unique inside the command: the client sends the answer back under this key.
CommandQuestionField has Text, LargeText, Integer, Decimal,
Boolean, Date, DateTime and Reference. The form uses the same inputs
as the command parameter prompt. Reference takes the dictionary by name (the
platform resolves it to the metaId) or by metaId.
The calls are on CommandContext, so every command kind has them. The untyped
bases (DictionaryCommandBase, DocumentCommandBase, UserCommandBase and
the list variants) also expose Confirm, Choose and Ask as protected
methods, so you can call them without context..
How it works: two runs, not a paused thread#
The server does not keep a request open while the user thinks. The command runs twice:
- The command calls
Confirm. There is no answer yet, so the call throwsCommandQuestionException. CommandExecutorcatches it and returns a result withawaitingAnswer: true,success: falseand oneAskclient action. The action carries the key, the kind (confirm,chooseorform), the text, the buttons and the fields.- The client (
executeWithAnswersinfrontend/src/lib/commandQuestions.ts) shows the dialog throughCommandQuestionHost. - The client runs the same command again, with the same selection and
parameters, plus
answers: { "negative": true }in the request body. - This time
Confirmfinds the answer and returns it. The command continues.
Answers accumulate. A command that asks three questions runs four times, and each run sends all the answers given so far. The client stops after 20 questions in a row, which protects it from a command that asks the same question forever.
Closing the dialog cancels the command. The client gets cancelled: true and
shows no error, because the user chose not to continue.
Warning
Everything the command did before the question happens again on the next run. Ask before you write anything: before
SaveDocumentAsync, before creating records, before calling a service that posts. Messages added withCreateUserMessagebefore the question are not delivered.
Warning
Do not wrap a question in
catch (Exception). The catch swallowsCommandQuestionException, and the command continues as if the user had answered.
Where it works#
| Caller | Questions |
|---|---|
POST /api/commands2/{kind}/{id}/execute and POST /api/commands/{id}/execute |
the request body takes answers; the result has awaitingAnswer |
| The command bar, ribbon user commands, layout buttons, command buttons widget, hotkeys, "Run" in the editor | the dialog appears; they all go through api.executeCommand2 / api.executeCommand |
| Integration tests | pass answers:; see below |
Mobile sync (ExecuteCommand push item) |
not supported: the push fails with the question text |
Testing a command that asks#
The test harness takes the answers as a named argument. Without them the result reports the open question.
var id = await Db.FindCommandIdAsync("document", "PostOrder");
var first = await Db.ExecuteDocumentCommandAsync(id, orderId);
Assert.IsTrue(first.AwaitingAnswer, "asks before posting");
Assert.AreEqual("negative", first.QuestionKey, "asks about the negative stock");
var posted = await Db.ExecuteDocumentCommandAsync(id, orderId,
answers: new Dictionary<string, object?> { ["negative"] = true });
Assert.IsTrue(posted.Success, "posts after Yes: {0}", posted.Message);
The value for Confirm is true or false. The value for Choose is the
button caption. The value for Ask is a dictionary from field name to value.
What breaks if you ask outside a command#
- Object events (
OnBeforeSave, field hooks) and services called outside a command cannot pause for UI — the save transaction cannot be replayed safely. - The mobile client does not implement command questions yet.
- Standalone form modules have their own command surface; do not assume the document-command question API applies there without checking.
Where next#
- Ask from a command: the recipe.
- Chain of Command: wrapping a command that asks still
needs
nextAsync, and the wrapper sees the same answers. - What a script can do: services and APIs available to a command script.
- Document commands for analysts: how a command moves a document between subtypes.