An action defines an operation that Bixby can perform on behalf of a user. If concepts are nouns, actions are verbs. Examples of actions include the following:
FindRestaurants
: search for restaurants.ConvertTemperature
: perform a temperature conversion computation.BookHotel
: perform a transactional operation that books a hotel room.In programming terms, you can think of an action model as an interface specification, describing the action inputs and action output. Simple functionality in your Bixby capsule might be possible entirely within action models. More complicated work, such as making API requests and performing computations, requires you to write a JavaScript function. Action models are connected to an implementation using an endpoint declaration (endpoints.bxb
). A local endpoint connects the action to a JavaScript function in your capsule that will be executed within Bixby; a remote endpoint connects the action to an API on an external server that will accept the action inputs and return the expected output.
As you start to build out your actions, make sure that you have read Bixby's Design Guidelines so that your models follow a consistent user experience.
For an example of how to write simple functions entirely with models and no JavaScript, see the No Code Design Patterns sample capsule.
Actions have one or more input
concepts specified by name, an output
concept, and a type
. Actions can, at certain points, also define dialog for Bixby.
An action's type, such as Calculation
, Constructor
, or Search
, tells Bixby about the action's purpose, and is important in planning the execution graph. The Side Effects section, below, discusses some action types; read the type
reference documentation for a complete discussion of the various types available. If you don't specify a type
for your action, it defaults to Search
, the most general type with the fewest limitations.
An action can have more than one input
, but it cannot have more than one input
of the same concept type. If you were building an action for searching flights, for example, you could not specify the departure and arrival airport inputs as both having types of Airport
; you would need a DepartureAirport
input type and an ArrivalAirport
input type, which could both be assigned roles of Airport
.
As with concept properties, action inputs can have cardinality constraints specified. The min
constraint can define if an input is optional or required, and max
can define if an input is single or multi-cardinal.
action (RollDice) {
collect{
input (numDice) {
type (NumDiceConcept)
min (Required)
max (One)
}
input (numSides) {
type (NumSidesConcept)
min (Required)
max (One)
}
}
output (RollResultConcept)
type (Calculation)
}
Keep in mind that, if a user request doesn't fulfill the cardinality for inputs, execution halts. Bixby communicates this requirement to the user via a prompt.
If an input is required, but no value is specified, the system provides an elicitation prompt, eliciting a value from the user. This is a violation of the min
constraint. If an input is single cardinal, but you get more than one value, the system prompts with a selection prompt. This is a violation of the max constraint.
Action outputs do not enforce cardinality, and can return any number of values for the specified concept type. If the output of an action is consumed by the input of another action, the cardinality is enforced by that input. An output of a single value, or zero values, can match either a single or multi-cardinal input. An output of two or more values, if matched with a single cardinality input, will cause Bixby to prompt the user to choose only one of the values.
Some actions have side effects, while others do not. You would expect FindRestaurants
or ConvertTemperature
to run without side effects, while BookHotel
models an action that changes state in another system. When defining an action, you should choose an appropriate action type to tell Bixby whether the action will have side effects.
The following non-transactional action types indicate that the action has no side-effects on external systems. When choosing an action type, select the narrowest possible value. Certain types are for specific features, so choose the type that best fits your action's purpose. For more information and examples of each action type, see the following linked descriptions.
Calculation
: isolated computation on the inputs, guaranteed to return a result.Constructor
: isolated construction of the output using only the provided inputs, and no external services.Fetch
: simple lookup of additional data from a single input.Search
: search action, using the inputs as constraints.If your action will have side effects on external systems, use a transactional action type. You can learn more about these types in the Transactional Workflows Developers' Guide.
The following action definition, FindShoe
, takes multiple inputs as search constraints and uses the Search
type:
action (FindShoe) {
type (Search)
collect {
input (name) {
type (Name)
min (Optional)
}
input (type) {
type (Type)
min (Optional)
}
input (minPrice) {
type (money.MinPrice)
min (Optional)
max (One)
}
input (maxPrice) {
type (money.MaxPrice)
min (Optional)
max (One)
}
}
output (Shoe)
}
To give you more control over selections made on the user’s behalf, you can use selection rules as a way for you to better control the selection of ambiguous input values instead of prompting the user. These selection rules are invoked whenever a maximum cardinality check is violated, so if max
is set to Many
, selection rules cannot be used.
Within the default-select
key, you enable selection learning using the with-learning
key. You then add rules using the new with-rule
key.
You can also add other selection learning behaviors (like NoPreferences
) as options.
Within selection rules, you can use select-first
and select-min
to choose the first candidate value or the lowest scoring value, respectively. You can optionally use sort-key
within select-first
to re-sort a list of inputs. If sort-key
is absent, Bixby selects the first input as-is without sorting. You can use multiple sort-key
’s in order to perform a compound sort and further refine select-first
rules.
Below is an example of choosing a size
for creating a shirt Item
. It uses an expression in select-min
to determine a default size.
action (CreateItem) {
type (Constructor)
collect {
input (shirt) {
type (Shirt)
min (Required)
default-init {
intent {
goal: FindShirt
}
}
}
input-group (itemProperties) {
requires (OneOrMoreOf)
collect {
input (size) {
type (Size)
min (Optional)
//Lists all available options - this is used by default-select
default-init {
intent {
goal: Size
value-set: Size { Size(ExtraSmall) Size(Small) Size(Medium) Size(Large) Size(ExtraLarge) }
}
}
default-select {
//picks Medium, otherwise pick first item from the list of candidates
with-rule {
select-min {
expression (exists(size) && size == 'Medium' ? 0 : 1)
}
}
with-learning {
}
}
}
input (quantity) {
type (Quantity)
min (Optional)
default-init {
intent {
goal: Quantity(1)
}
}
}
}
}
}
output (Item)
}
For information on the difference between Selection Rules and Selection Learning, see the Selection Learning topic.
To understand how selection rules and selection learning behave, see Selection Behavior.
When defining actions, you can specify cardinality on individual inputs.
Extending this, you can use input-group
keys to apply this requirement across multiple inputs.
For example, in the Space Resorts sample capsule, if a user wants to find a resort, you can direct them to be specific about the type of resort they want. With input groups, the user can ask for resorts on Mars, resorts with spas, or even resorts on Mars with spas, but they must specify at least one of the constraints:
action (SelectResort) {
description (Select a HabitatProd from a list by name)
type (Calculation)
collect {
input (resorts) {
type (SpaceResort)
min (Required)
max (Many)
default-init {
//get the candidates from the input-view (prompt context)
intent {
goal-set: SpaceResort {$expr("getPreviousPage('SpaceResort')")}
}
}
}
input-group(search) {
requires (OneOrMoreOf)
collect {
input (name) {
description (space resort name)
type (Name)
min (Optional)
}
input (planet) {
description (space resort name)
type (Planet)
min (Optional)
}
input(searchCriteria) {
description (search criteria)
type (SearchCriteria)
min (Optional)
max (Many)
}
}
}
}
output (SpaceResort) {
on-empty {
// TODO
}
throws {
error (MultipleMatches) {
property (matches) {
type (SpaceResort)
min (Required)
max (Many)
}
on-catch {
replan {
intent {
goal { SpaceResort @prompt-behavior(AlwaysSelection) }
value { $expr(matches) }
}
}
}
}
}
}
}
Notice that the cardinality has a slightly different meaning for input-group
s. With input-group
keys, the min
value determines whether an input
is required, and the max
value determines the maximum number of input
declarations within an input-group
to allow in a query.
In this example, users can specify multiple inputs:
"Find me a space resort on Mars that's good for kids and has a spa."
Marking an input as iterable
rather than marking it as multi-cardinal will result in the action being called again for each input of that type which presents itself. For example, FindRestaurants
has the searchRegion
set as iterable:
input (searchRegion) {
iterable
type (SearchRegion)
min (Required)
}
An utterance such as "Find restaurants in San Jose and San Francisco" calls the action FindRestaurants
twice, once for each city.
Note that iterable
should be used with care because, if there are many inputs, it will call the action many times. There cannot be more than one iterable
input at a time because of a large number of combinations are possible. The results of this query are merged into a single list.
Action declarations also support simple validation logic. A validation consists of validate
and a condition
that can trigger a halt
, prompt
, or replan
.
For example, you could implement an action that cancels a shirt order. During cancellation, you can ensure that the shirt order exists. You can implement this using action input validation:
action (CancelCommittedOrder) {
type (CancelActivity)
confirm {
by (core.Confirmation)
}
collect {
input(receipt){
type(Receipt)
min (Optional)
default-init {
intent {
goal: FindLastReceipt
}
}
validate {
if (!exists(receipt)) {
halt {
dialog {
template("Not sure what to cancel. I didn't find any recent shirt orders.")
}
}
}
if (exists(receipt) && receipt.orderState != 'Ordered') {
halt {
dialog{
template("This order is already #{value (receipt.orderState)}!")
}
}
}
}
}
}
output(Receipt)
}
Declaring input validation in the action model is often the best solution because the validation rules become part of the model. This is often the cleanest way to perform validation. Alternatively, you can move the validation logic into your action JavaScript and use checked errors by Throwing Exceptions, which can be more flexible.
Input validation happens after default-init
blocks are evaluated, and after cardinality and requires
constraints are checked. Where the validate
key appears in the input block doesn't matter.
You can see an example of Input Validation in the Input Validation and Error Handling sample capsule.
Checked errors allow you to throw a named error through JavaScript. You can then catch and handle the error in the action. The first part of error handling, involves handling the error through your action model, which we cover here. The second part involves throwing the error, which is handled through JavaScript in your action implementation.
Actions can declare handling of specific checked errors through the throws
key in the action's output
. Here's an example that declares an UnsupportedSearchCriteria
error, which is developer defined:
action (FindSpaceResorts) {
description (Find space resorts)
type (Search)
collect {
input (name) {
type (Name)
}
input (planet) {
type (Planet)
}
input (searchCriteria) {
type (SearchCriteria)
max (Many)
}
}
output (SpaceResort) {
throws {
error (UnsupportedSearchCriteria) {
on-catch {
// TODO: drop the unsupported criterion with a message and continue
halt {
dialog {
macro (UNSUPPORTED_SEARCH_OPTION)
}
}
}
}
unknown-error {
on-catch {
halt {
dialog {
macro (UNKNOWN_ERROR)
}
}
}
}
}
}
}
Each checked error declaration specifies an error code. We recommend that you implement error handling that addresses the error gracefully. For example, if the error involves authorization, you might want to take the user through an authorization flow instead of simply presenting an authorization error.
In addition to dialog, you can use other effects such as replace
, halt
, drop
, and replan
.
Another type of checked error might mean that the action needs more clarification or additional input from the user. You can handle these situations with a prompt
:
output (SelectedItem) {
throws {
error(MultipleMatches) {
property (matches) {
type (Item)
max (Many)
}
on-catch {
//prompt for matched items, clear searchTerm
prompt (items) {
required-value (true)
single-value (true)
candidates (matches)
}
}
}
}
on-empty {
//prompt for items, clear searchTerm
ordered-effects {
drop (searchTerm)
prompt (items) {
required-value (true)
single-value (true)
candidates (items)
}
}
}
}
In other cases, you can offer the user a replacement intent, using replan
:
output (HabitatPod) {
throws {
error (MultipleMatches) {
property (matches) {
type (HabitatPod)
min (Required)
max (Many)
}
on-catch {
replan {
intent {
goal { HabitatPod @prompt-behavior(AlwaysSelection) }
value { $expr(matches) }
}
}
}
}
}
}
A checked error can also declare that it expects a property
as part of the error message and uses the property
in the error dialog. You first declare the concept type that you are expecting. Then, in the JavaScript action implementation where you throw an error, you can pass along the data for the concept.
For example, this RequiredOptionNotSpecified
error expects a property optionCategory
:
output (Order) {
throws {
error (RequiredOptionNotSpecified) {
property (optionCategory) {
type (ProductOptionCategory)
description (The optionCategory to prompt for!)
}
on-catch {
prompt (options) {
min (optionCategory.minCardinality)
max (optionCategory.maxCardinality)
mode (Add)
candidates (optionCategory.options)
}
}
}
}
}
You can also catch unnamed or unknown errors. You should do this directly in the action that might produce them.
Here is an example of how you can use unknown-error
within an action:
output (SpaceResort) {
throws {
error (UnsupportedSearchCriteria) {
on-catch {
// TODO: drop the unsupported criterion with a message and continue
halt {
dialog {
macro (UNSUPPORTED_SEARCH_OPTION)
}
}
}
}
unknown-error {
on-catch {
halt {
dialog {
macro (UNKNOWN_ERROR)
}
}
}
}
}
}
To learn more about the JavaScript implementation of error handling, read about Throwing Exceptions or read the reference documentation on throws
.
If you need to change the effects of your error handling during runtime, you can also override behavior with runtime flags.
You can see more examples of error handling in the Input Validation and Error Handling sample capsule.