Inquiries
Inquiries allow you to pause a workflow to wait for additional information. This is done by using
the core.ask
action. The idea is to allow you to “ask a question” in the middle of a workflow.
This could be a question like “do I have approval to continue?” or “what is the second factor I
should provide to this authentication service?”
These use cases (and others) require the ability to pause a workflow mid-execution and wait for additional information. Inquiries make this possible. This document explains how to use them.
core.ask
The best way to get started using Inquiries is to check out the core action - core.ask
-
and start using it in your workflows. This action is built on the inquirer
runner type,
which performs the bulk of the logic required to pause workflows and wait for a response.
~$ st2 action get core.ask
+-------------+----------------------------------------------------------+
| Property | Value |
+-------------+----------------------------------------------------------+
| id | 59a8c27732ed3553ceb2dec4 |
| uid | action:core:ask |
| ref | core.ask |
| pack | core |
| name | ask |
| description | Action for initiating an Inquiry (usually in a workflow) |
| enabled | True |
| entry_point | |
| runner_type | inquirer |
| parameters | |
| notify | |
| tags | |
+-------------+----------------------------------------------------------+
The inquirer
runner imposes a number of parameters that are, in turn, required by the
core.ask
action:
Parameter |
Description |
---|---|
schema |
A JSON schema that will be used to validate the response data. A basic schema will be provided by default, or you can provide one here. Only valid responses will cause the action to succeed, and the workflow to continue. |
ttl |
Time (in minutes) until an unacknowledged Inquiry is garbage-collected. Set to 0 to disable garbage collection for this Inquiry. NOTE - Inquiry garbage collection is not enabled by default, so this field does nothing unless it is turned on. See Garbage Collection for Inquiries for more info. |
roles |
A list of RBAC roles that are permitted to respond to the action. Defaults to empty list, which permits all roles. This requires enterprise features on StackStorm 3.2 and before, but is available on StackStorm 3.4 and later if Role Based Access Control is enabled. |
users |
A list of users that are permitted to respond to the action. Defaults to empty list, which permits all users. |
route |
An arbitrary string that can be used to filter different Inquiries inside rules. This can be helpful for deciding who to notify of an incoming Inquiry. See Notifying Users of Inquiries using Rules for more info. |
Using core.ask
in a Workflow
While you can use this action on its own (i.e. with st2 run
), the real value comes from using
it in a Workflow.
The core.ask
action supports a number of parameters, but the most important one by far is the
schema
parameter. This parameter defines exactly what kind of responses will satisfy the
Inquiry, and allow the workflow to continue. When users respond to this Inquiry, their response
must come in the form of a JSON payload that will satisfy this schema. We cover responses in
Responding to an Inquiry below - the st2
client makes this
pretty easy.
Now we’ll use this action in an example workflow. The following example shows a simple ActionChain
with two tasks. task1
executes the core.ask
action and passes in a few parameters:
chain:
- name: task1
ref: core.ask
params:
route: developers
schema:
type: object
properties:
secondfactor:
type: string
description: Please enter second factor for authenticating to "foo" service
required: True
on-success: "task2"
- name: task2
ref: core.local
params:
cmd: echo "We can now authenticate to "foo" service with {{ task1.result.response.secondfactor }}"
Note that we’re using a Jinja snippet in task2
to access and make use of the value that we’re
asking for. In this example we’re simply printing this to the screen, but the
<task>.result.response
dictionary will contain all of the values that satisfy our schema. More
on this later.
We can run this workflow to see its execution:
~$ st2 run examples.chain-test-inquiry
.
id: 59d1ecb632ed353f1f340898
action.ref: examples.chain-test-inquiry
parameters: None
status: paused
result_task: task1
result:
roles: []
route: developers
schema:
properties:
secondfactor:
description: Please enter second factor for authenticating to "foo" service
required: true
type: string
type: object
ttl: 1440
users: []
start_timestamp: 2017-10-02T07:37:26.854217Z
end_timestamp: None
+--------------------------+---------+-------+----------+-------------------------------+
| id | status | task | action | start_timestamp |
+--------------------------+---------+-------+----------+-------------------------------+
| 59d1ecb732ed353ec4aa9a5a | pending | task1 | core.ask | Mon, 02 Oct 2017 07:37:27 UTC |
+--------------------------+---------+-------+----------+-------------------------------+
As you can see, the status of our ActionChain is paused
. Note that task2
hasn’t even been
scheduled, because the use of the core.ask
action prevented further tasks from running. You’ll
also notice that the status for task1
is pending
. This indicates to us that this particular
Inquiry has not yet received a valid response, and is currently blocking the Workflow execution.
You can also use core.ask
to ask a question within Orquesta workflows:
version: 1.0
description: A basic workflow that demonstrates inquiry.
tasks:
start:
action: core.echo message="Automation started."
next:
- when: <% succeeded() %>
do: get_approval
get_approval:
action: core.ask
input:
schema:
type: object
properties:
approved:
type: boolean
description: "Continue?"
required: True
next:
- when: <% succeeded() %>
do: finish
- when: <% failed() %>
do: stop
finish:
action: core.echo message="Automation completed."
stop:
action: core.echo message="Automation stopped."
next:
- do: fail
When encountering an Inquiry, StackStorm will send a request to Orquesta to pause execution of a workflow, just like we saw previously with ActionChains:
~$ st2 run examples.orquesta-ask-basic
.
id: 59a9c99032ed3553fb738c83
action.ref: examples.orquesta-ask-basic
parameters: None
status: paused
start_timestamp: 2017-09-01T20:56:48.630380Z
end_timestamp: None
+--------------------------+---------+-------+----------+-------------------------------+
| id | status | task | action | start_timestamp |
+--------------------------+---------+-------+----------+-------------------------------+
| 59a9c99132ed3553fb738c86 | pending | task1 | core.ask | Fri, 01 Sep 2017 20:56:49 UTC |
+--------------------------+---------+-------+----------+-------------------------------+
Note
At the time of this writing, the Inquiry ID is the same as the action execution ID that raised
it. So if you’re curious which workflow a given Inquiry is part of, use the same ID with the
st2 execution get
command.
The example below shows a slightly extended version of the basic Orquesta workflow above. This one uses a JSON schema to query and verify multiple parameters and shows how to refer to single values for further processing.
version: 1.0
description: A basic workflow that demonstrates inquiry with multiple parameters.
tasks:
start:
action: core.echo message="Automation started."
next:
- when: <% succeeded() %>
do: get_approval
get_approval:
action: core.ask
input:
schema:
type: object
properties:
approved:
type: boolean
description: "Continue?"
required: True
department_id:
type: number
description: "Your department ID:"
required: True
extra_output:
type: string
description: "Your message to echo next if you approve to continue:"
ttl: 60
next:
- when: <% task(get_approval).result.response.containsKey("extra_output") and task(get_approval).result.response.approved = true %>
publish:
- custom_output: <% task(get_approval).result.response.extra_output %>
- approver_department_id: <% task(get_approval).result.response.department_id %>
do: echo_extra_message
- when: <% not task(get_approval).result.response.containsKey("extra_output") and task(get_approval).result.response.approved = true %>
publish:
- approver_department_id: <% task(get_approval).result.response.department_id %>
do: finish
- when: <% task(get_approval).result.response.approved = false %>
do: stop
echo_extra_message:
action: core.echo message="Extra info <% ctx('custom_output') %>."
next:
- when: <% succeeded() %>
do: finish
finish:
action: core.echo message="Approved by department <% ctx('approver_department_id') %>. Automation completed."
stop:
action: core.echo message="Automation stopped."
next:
- do: fail
Notifying Users of Inquiries using Rules
When a new Inquiry is raised, a dedicated trigger - core.st2.generic.inquiry
- is used. This
trigger can be consumed in Rules, and you can use an action or a workflow to provide notification
to the relevant party. For instance, using Slack:
---
name: "notify_inquiry"
pack: "examples"
description: Notify relevant users of an Inquiry action
enabled: false
trigger:
type: core.st2.generic.inquiry
action:
ref: slack.post_message
parameters:
channel: "#{{ trigger.route }}"
message: 'Inquiry {{trigger.id}} is awaiting an approval action'
Note how this Rule uses the route
field to determine to which Slack channel the notification
should be sent. You could also use this in the Rule criteria as well, and set up different
notification actions depending on the value of route
.
Responding to an Inquiry
In order to resume a Workflow that’s been paused by an Inquiry, a response must be provided to that Inquiry, and the response must come in the form of JSON data that validates against the schema in use by that particular Inquiry instance.
In order to respond to an Inquiry, we need its ID. We would already have this if we wrote a Rule
like shown in the previous section, but we could also use the st2 inquiry list
command to view
all outstanding inquiries:
~$ st2 inquiry list
+--------------------------+-------+-------+------------+------+
| id | roles | users | route | ttl |
+--------------------------+-------+-------+------------+------+
| 59d1ecb732ed353ec4aa9a5a | | | developers | 1440 |
+--------------------------+-------+-------+------------+------+
Like most other resources in StackStorm, we can use the get
subcommand to retrieve details
about this Inquiry, using its ID provided in the previous output:
~$ st2 inquiry get 59d1ecb732ed353ec4aa9a5a
+----------+--------------------------------------------------------------+
| Property | Value |
+----------+--------------------------------------------------------------+
| id | 59d1ecb732ed353ec4aa9a5a |
| roles | |
| users | |
| route | developers |
| ttl | 1440 |
| schema | { |
| | "type": "object", |
| | "properties": { |
| | "secondfactor": { |
| | "required": true, |
| | "type": "string", |
| | "description": "Please enter second factor for |
| | authenticating to "foo" service" |
| | } |
| | } |
| | } |
+----------+--------------------------------------------------------------+
In this view, we see the schema in use requires a single key: secondfactor
, whose value must
be a string.
Note
You can omit the schema
parameter when using core.ask
, and a basic schema will be used
as default - only requiring a single boolean value to continue the workflow. In this example,
we’ve provided our own schema that allows us to use the retrieved value in a later task of the
workflow. This allows you to “inject” data into a workflow mid-execution, rather than rely
solely on parameters.
Fortunately, the st2
client makes it easy to provide a valid response; when you run the
command st2 inquiry respond <inquiry id>
, it will step through each of these values, prompting
you with the provided description. You simply respond to each prompt:
~$ st2 inquiry respond 59d1ecb732ed353ec4aa9a5a
secondfactor: bar
Please enter second factor for authenticating to "foo" service
Response accepted. Successful response data to follow...
+----------+---------------------------+
| Property | Value |
+----------+---------------------------+
| id | 59d1ecb732ed353ec4aa9a5a |
| response | { |
| | "secondfactor": "bar" |
| | } |
+----------+---------------------------+
It’s very important that each property in the response schema has a proper description, as shown in the default example, as this is what prompts the user for required values when it’s time to respond.
Since the st2
client has a handle on the schema being used for an Inquiry, it can guide you to
provide the right datatypes for each attribute, and won’t continue until you do. For instance, if
our schema required a boolean
value, an integer would be rejected client-side:
~$ st2 inquiry respond 59ab26af32ed35752062d2dc
continue (boolean): 123
Does not look like boolean. Pick from [false, no, nope, nah, n, 1, 0, y, yes, true]
Should we continue?
However, not every response can be done interactively. You may even want to script some or all of
your Inquiry responses, and may be using tools like jq to craft your own JSON payload for a
response and wish to simply provide this to the CLI. The -r
flag can be used for this:
~$ st2 inquiry respond -r '{"secondfactor": "bar"}' 59d1ecb732ed353ec4aa9a5a
Response accepted. Successful response data to follow...
+----------+---------------------------+
| Property | Value |
+----------+---------------------------+
| id | 59d1ecb732ed353ec4aa9a5a |
| response | { |
| | "secondfactor": "bar" |
| | } |
+----------+---------------------------+
Note that this effectively bypasses any client-side validation, so it’s possible to send a JSON payload that doesn’t validate against the schema. However, the API is the ultimate authority on validating an Inquiry response, so in this case, you’ll still get an error in return:
~$ st2 inquiry respond -r '{"secondfactor": 123}' 59d1ecb732ed353ec4aa9a5a
ERROR: 400 Client Error: Bad Request
MESSAGE: Response did not pass schema validation. for url: http://127.0.0.1:9101/exp/inquiries/59ab26af32ed35752062d2dc
Once an acceptable response is provided, the workflow resumes:
~$ st2 execution get 59d1ecb632ed353f1f340898
id: 59d1ecb632ed353f1f340898
action.ref: examples.chain-test-inquiry
parameters: None
status: succeeded (468s elapsed)
result_task: task2
result:
failed: false
return_code: 0
stderr: ''
stdout: We can now authenticate to foo service with bar
succeeded: true
start_timestamp: 2017-10-02T07:37:26.854217Z
end_timestamp: 2017-10-02T07:45:14.123405Z
+--------------------------+------------------------+-------+------------+-------------------------------+
| id | status | task | action | start_timestamp |
+--------------------------+------------------------+-------+------------+-------------------------------+
| 59d1ecb732ed353ec4aa9a5a | succeeded (0s elapsed) | task1 | core.ask | Mon, 02 Oct 2017 07:37:27 UTC |
| 59d1ee8932ed353ec4aa9a5d | succeeded (1s elapsed) | task2 | core.local | Mon, 02 Oct 2017 07:45:12 UTC |
+--------------------------+------------------------+-------+------------+-------------------------------+
Note that the stdout
for task2
(and subsequently, this ActionChain) is “We can now
authenticate to foo service with bar”. If you recall, this was because we were using a Jinja
snippet to print the value of secondfactor
in our response. We just printed the phrase to the
screen in this example, but you can just as easily use this to pass a value into another action in
your workflow.
The st2
pack also now contains an inquiry.respond
action, which may be useful for
responding to inquiries within another workflow:
~$ st2 inquiry get 5a1f4411c4da5f4486b09364
+----------+--------------------------------------------------------------+
| Property | Value |
+----------+--------------------------------------------------------------+
| id | 5a1f4411c4da5f4486b09364 |
| roles | |
| users | |
| route | developers |
| ttl | 1440 |
| schema | { |
| | "type": "object", |
| | "properties": { |
| | "secondfactor": { |
| | "required": true, |
| | "type": "string", |
| | "description": "Please enter second factor for |
| | authenticating to "foo" service" |
| | } |
| | } |
| | } |
+----------+--------------------------------------------------------------+
vagrant@st2vagrant:~$ st2 run st2.inquiry.respond id=5a1f4411c4da5f4486b09364 response='{"secondfactor": "foo"}'
.
id: 5a1f444ec4da5f4486b09366
status: succeeded
parameters:
id: 5a1f4411c4da5f4486b09364
response:
secondfactor: '********'
result:
exit_code: 0
result: null
stderr: ''
stdout: ''
Note
You’ll notice that the value for the key secondfactor
is masked within the response body
in the execution output for this action. The st2.inquiry.respond
action doesn’t actually
know the inquiry response schema at all - it is merely a thin layer on top of the StackStorm
client. As a result, it doesn’t know which fields are marked with the secret
attribute.
To avoid potentially leaking secrets, all field values are masked in this way for the output
of this action, regardless of whether or not the schema has declared them as secrets.
The st2
pack also contains an action alias for responding to Inquiries via ChatOps. Using this
alias, you can respond to an Inquiry within Slack, as an example:
!st2 respond to inquiry 5a1f4860c4da5f4486b093bf with {“secondfactor”: “supersecret”}
Securing Inquiries
Inquiries work a little differently from other system resources with it comes to granting permissions
to them via RBAC. The users
and roles
parameters for the core.ask
action
allow you to control who can respond to a specific inquiry, right in the workflow. With this granularity
being offered in parameters, RBAC for Inquiries is a bit simpler, focusing broadly on who has access
to Inquiries in general, leaving specific access control to the action parameters.
For example, rather than specifying a particular Inquiry when constructing a role, all Inquiry
UIDs should be specified as inquiry:
. Whatever permissions are granted in the role are granted
to all inquiries:
---
name: "inquiry_role_respond"
description: "Role which grants inquiry powers"
permission_grants:
- resource_uid: "inquiry:"
permission_types:
- "inquiry_respond"
Inquiries also honor execution permissions for the workflow they were generated from. For
instance, if user inherit
has action_execute
permissions on the workflow
examples.orquesta-ask-basic
, they don’t need to be explicitly granted inquiry_respond
permissions - this is done automatically.
The following is an example role that only grants permissions to execute a workflow that contains
a core.ask
action, but doesn’t explicitly grant inquiry_respond
permissions. However, any
user that’s been assigned to this role will still be permitted to respond.
---
name: "inquiry_role_inherit"
description: "Role which only grants action powers - will inherit inquiry_respond"
permission_grants:
# Grant to run the workflow
- resource_uid: "action:examples:orquesta-ask-basic"
permission_types:
- "action_execute"
- "action_view"
# Grant to run the core.ask action
- resource_uid: "action:core:ask"
permission_types:
- "action_execute"
- "action_view"
# Grant to list runners (allows us to test this with `st2 run`)
- resource_uid: "runner_type:orquesta"
permission_types:
- "runner_type_list"
To lock down a specific Inquiry to a set of users or RBAC roles (the latter of which is only
available with enterprise features), the users
and roles
parameters
should be used. These offer additional restriction on a per-Inquiry basis, but they don’t remove
any restrictions imposed on the aforementioned RBAC settings, if any. These parameter-based
restrictions are cumulative with any existing RBAC restrictions.
The users
parameter is a list of users that are permitted to respond to this specific instance
of an Inquiry. Similarly, roles
controls which RBAC roles (assuming enterprise features) are
allowed to respond to this specific Inquiry. The default value for both of these parameters is an
empty list, which permits all. The following ActionChain invokes a core.local
action, passing
a list into the users
parameter that specifies only st2responduser
is able to respond:
chain:
- name: task1
ref: core.ask
params:
route: developers
users:
- st2responduser
schema:
type: object
properties:
secondfactor:
type: string
description: Please enter second factor for authenticating to "foo" service
required: True
on-success: "task2"
- name: task2
ref: core.local
params:
cmd: echo "We can now authenticate to "foo" service with {{ task1.result.response.secondfactor }}"
All other users attempting to respond will be rejected, even if they are granted
inquiry_respond
RBAC permissions.
Garbage Collection for Inquiries
As alluded to in Purging Old Operational Data, the
st2garbagecollector
service is also responsible for cleaning up old Inquiries. This is done by
comparing the ttl
parameter of an Inquiry with its start time. The ttl
field is the number
of minutes since the start time the Inquiry will be allowed to receive responses, before it is
cleaned up.
Unlike garbage collection for trigger-instances, or action executions, Inquiries are not deleted when they’re “cleaned up”. Rather, they’re marked as “timed out”. This allows workflows to make different decisions based on whether or not an Inquiry was responded to successfully, or if the TTL expired waiting for a response.
To configure garbage collection for Inquiries, you first need to enable this globally. Unlike
trigger-instances and action executions, /etc/st2/st2.conf
only requires a single boolean
parameter to enable Inquiry garbage colllection:
[garbagecollector]
# By default, this value is False
purge_inquiries = True
Once done, each Inquiry has its own ttl
configured via parameters. The default is 1440 - 24
hours. However, this can be easily overridden for a inquiry by specifying the ttl
in a
parameter for the core.ask
action, like in the following Orquesta workflow:
version: 1.0
description: A basic workflow that demonstrates inquiry.
tasks:
start:
action: core.echo message="Automation started."
next:
- when: <% succeeded() %>
do: get_approval
get_approval:
action: core.ask
input:
ttl: 60
route: developers
next:
- when: <% succeeded() %>
do: finish
- when: <% failed() %>
do: stop
finish:
action: core.echo message="Automation completed."
stop:
action: core.echo message="Automation stopped."
next:
- do: fail
Note
Even if Inquiry garbage collection is enabled globally in the st2 config, you can use a TTL value of 0 to disable garbage collection for a specific Inquiry.
Once this option has been enabled, and the st2garbagecollector
service is started, it will
begin periodically looking for Inquiries that have been in a pending
state beyond their
configured ttl
. If we didn’t respond to the above inquiry within 60 minutes, then get_approval
would be marked “timeout”, and the workflow would go the stop
task.