Bixby supports the following ways to implement action models:
There are advantages and disadvantages when using remote endpoints over local endpoints.
These are the advantages:
These are the disadvantages:
localhost
(see the Localhost Tunneling section for details on how you can achieve this).You create action endpoints (both remote and local) through an endpoints.bxb
file that defines the action, including the inputs the action will accept and the endpoints that the action will call. You can have multiple endpoints.bxb
files to support different types of endpoints, but they must be placed within your capsule resources
folder.
/resources/base/
: endpoints that apply to all locales/resources/base/%locale%/
: endpoints that apply to a specific locale, such as en-us
.Endpoint definitions follow a similar format for all endpoint types. This is the endpoint.bxb
file for the Shoe sample capsule:
endpoints {
action-endpoints {
action-endpoint (FindShoe) {
accepted-inputs (name, type, minPrice, maxPrice)
local-endpoint ("FindShoe.js")
}
action-endpoint (FindAccessories) {
accepted-inputs (shoe)
local-endpoint ("FindAccessories.js")
}
}
}
action-endpoint
key defines and names a specific endpoint, such as FindShoe
.local-endpoint
key specifies the name of the JavaScript file that implements the action, located in the capsule's code/
folder.remote-endpoint
key specifies the web services URL to call.capsule.properties
file.GET
or POST
.accepted-inputs
key defines the parameters that are passed to the action.accepted-inputs
(or the input
keys in the action model).$vivContext
parameter can be included. This will allow your actions to access user-specific information.For local endpoints, you must declare the local file (local-endpoint
) that contains the external action implementation as well as the inputs to accept (accepted-inputs
). As with the JavaScript that you use within capsule, you can use $vivContext
to access user-specific information. The example below shows three ways local endpoints can be specified: with and without $vivContext
, and in the third example, calling a specific function, getSock()
, within the FindSock.js
file.
endpoints {
action-endpoints {
// simple local-endpoint
action-endpoint (FindShoe) {
accepted-inputs (name, type, minPrice, maxPrice)
local-endpoint (FindShoe.js)
}
// simple local-endpoint with optional $vivContext input
action-endpoint (FindOtherShoe) {
local-endpoint (FindOtherShoe.js)
accepted-inputs (name, type, minPrice, maxPrice, $vivContext)
}
// simple local-endpoint, specifying a function
action-endpoint (FindSock) {
local-endpoint (FindAccessory.js::getSock)
accepted-inputs (name, type, minPrice, maxPrice)
}
}
}
You can then define an action implementation that returns values back to your capsule:
export const tests = [];
export const preconditions = [];
import SHOES from "./lib/shoes";
//FindShoe
export default function ({ name, type, minPrice, maxPrice }) {
var result = SHOES.filter(function (shoe) {
return (
(!name || shoe.name.toUpperCase() == name.toUpperCase()) &&
(!type || shoe.type.toUpperCase() == type.toUpperCase()) &&
(!maxPrice || shoe.price.value <= maxPrice.value) &&
(!minPrice || shoe.price.value >= minPrice.value)
);
});
return result;
}
The parameters passed to the JavaScript action are set by the accepted-inputs
key. In the example above, the parameters match this JavaScript function
:
accepted-inputs (name, type, minPrice, maxPrice)
The order does not matter, but the names between accepted-inputs
and the JavaScript function parameters must match.
If the accepted-inputs
line is not given, Bixby maps the inputs from the Action model to the JavaScript function. The FindShoe.bxb
action model contains all the necessary inputs:
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)
}
If you include an empty accepted-inputs
key, it will only map to the JavaScript function when there are no inputs at runtime. The following endpoint will never pass inputs to its JavaScript action:
action-endpoint (FindBusiness) {
accepted-inputs ()
local-endpoint (FindBusiness.js)
}
If any inputs are passed to FindBusiness
, then FindBusiness.js
will not be invoked.
When using local endpoints, we enforce JavaScript execution resource limits.
CPU running time is limited to 25 seconds for each JavaScript action. This includes any aspect of the full JavaScript execution including JavaScript code bootstrap, interpretation, and function result or failure handling.
Additionally, memory allocation is limited to 65 MB.
Bixby will terminate violators of these limits, and they are subject to change in the future.
For remote endpoints, you must declare the URL (remote-endpoint
) of your remote HTTP server's API, the inputs to accept (accepted-inputs
), and the HTTP method to use when calling the endpoint. Endpoints that accept parameters must use POST
or PUT
; you cannot use GET
to pass any concepts from Bixby. Parameters are passed in the HTTP request body as a JSON object.
You can implement your remote endpoints using any HTTP server technology and language you choose. The only requirements are that they accept and return JSON objects that map to Bixby concepts. As an example, here is a Bixby Shoe
concept from the HTTP API Call sample capsule and a JSON representation of a Shoe
:
This is the Bixby concept model:
structure (Shoe) {
property (name) {
type (Name)
min (Required)
}
property (description) {
type (Description)
min (Required)
}
property (type) {
type (Type)
min (Required)
}
property (price) {
type (money.Price)
min (Required)
}
//unused properties returned from API
property (id) {
type (Id)
min (Optional)
}
property (photo) {
type (Photo)
min (Optional)
}
}
This is its JSON representation:
{
name: "Mules",
description: "No fitting around the heel (i.e. they are backless).",
price: {
value: 65,
currencyType: {
currencyCode: "USD",
prefixSymbol: "$"
}
},
images: [{url:'/images/mules.jpg'}],
type: "Formal"
},
In that sample capsule, the FindShoeRemoteEndpoint
has a very simple model that is functionally identical to its local endpoint counterpart:
action (FindShoeRemoteEndpoint) {
type (Search)
description (Demonstrate how to use remote endpoints for a simple search.)
output (Shoe)
}
In the endpoints.bxb
file, this action has a matching remote endpoint defined. This is similar to how a local endpoint would be configured, but using the remote-endpoint
key and specifying a URL rather than specifying a JavaScript file. In this case, the endpoint takes no parameters, and simply returns a JSON array of shoe objects like the one shown above.
action-endpoint (FindShoeRemoteEndpoint) {
accepted-inputs ()
remote-endpoint ("https://my-json-server.typicode.com/bixbydevelopers/capsule-samples-collection/shoes") {
method (GET)
}
}
The {remote.url}
in the remote-endpoint
specification is a reference to a property specified in the capsule.properties
file:
config.default.remote.url=http://shoe.bixby.pro
If this endpoint did take a parameter like its local endpoint counterpart, it would need to be listed in the accepted-inputs
key.
accepted-inputs (type)
If you need to pass in HTTP headers, such as Content-Type: application/x-www-form-urlencoded
or Content-Type: application/json
, you can specify them using the header
key within the remote-endpoint
block:
endpoints {
action-endpoints {
action-endpoint (FindShoeRemoteEndpoint) {
accepted-inputs (type)
remote-endpoint ("{remote.url}/shoes") {
method (POST)
headers {
header (Content-Type: application/json)
}
}
}
}
}
Remote endpoints are always passed $vivContext
whether or not it is declared in the accepted-inputs
key. See Accessing User Context for details.
The HTTP API sample capsule has a working demonstration of remote-endpoint
in it, along with several demonstrations of using local endpoints with the HTTP API.
Since capsules are run through the Bixby platform, they don't have access to remote endpoint servers running on localhost
; the endpoints must be publicly accessible. This can make development and testing of remote endpoints difficult. While you can address this by hosting and constantly updating the remote endpoint code, you can also use tunneling to forward requests from your publicly available IP to your local machine. There are many tools out there which can be used to achieve this, such as Pagekite, localtunnel, or ngrok.
Be aware that exposing an http server running on localhost
publicly via tunneling has security implications that extend beyond the scope of this documentation. Please be cautious, do your research on security best practices, and only use test data when utilizing tools like those listed above.
If your capsule needs to interface with an existing web service that takes input or returns output in a format that's not directly compatible with a Bixby concept, you might not be able to use remote-endpoint
. In this case, you can write a local endpoint that uses Bixby's JavaScript HTTP library to talk to your API.
Suppose the Remote Endpoints example above did not return a JSON representation of the Shoe
concept, but instead returned an entirely different JSON structure:
{
"uuid": "0158139A-37A6-4748-8C5F-A0C36E97247B",
"name": "Boots",
"desc": "Covers the foot and the ankle and extends up the leg.",
"price": 70,
"currency": "USD",
"kind": "Boot"
}
By using a local endpoint, you could map the response onto the Shoe concept:
import http from 'http'
import console from 'console'
import config from 'config'
export function findShoe(input) {
const { type, $vivContext } = input
console.log('FindShoe filter by a specific type')
const options = {
format: 'json',
query: {
type: type,
},
}
const response = http.getUrl(config.get('remote.url') + '/shoes', options)
const currencySymbol = {
USD: '$',
EUR: '€',
}
let shoes = []
for (let item of response) {
shoes.push({
name: item.name,
description: item.desc,
price: {
value: item.price,
currencyType: {
currencyCode: item.currency,
prefixSymbol: currencySymbol[item.currency],
},
},
type: item.kind,
})
}
return shoes
}
The query being sent to the remote server is in the query
property of the options
object. The config.get()
JavaScript method is used to get the remote.url
property from the capsule.properties
file. The response
variable is assumed to be an array of JSON objects; the for
loop simply goes through each object, creating a new object in the proper format and adding it to the shoes
array. Finally, shoes
is returned to Bixby.
Remember that the specifics of the HTTP response are dependent on what the web service API returns. You need to use that API's documentation as a guide to reformatting the response object and, if necessary, the HTTP query object. You can use the format
option in the getUrl
options to help parse different kinds of responses; consult the HTTP Options documentation for details.
For more examples of working with APIs from local endpoints, review the HTTP API Call sample capsule.
If the $vivContext
parameter is passed to your endpoint, your action can read certain context information, such as the user's locale, timezone, and certain device information. For local endpoints, $vivContext
must be explicitly added to accepted-inputs
; for remote endpoints, it is always passed to the remote API. Consider the following endpoint declaration:
endpoints {
action-endpoints {
action-endpoint (AccessVivContext) {
accepted-inputs ($vivContext)
local-endpoint ("AccessVivContext.js")
}
}
}
Now $vivContext
can be accessed in your implementation function like any other JavaScript object.
// Request access to the $vivContext by declaring it as a parameter
export default function accessVivContext({ $vivContext }) {
return JSON.stringify($vivContext, undefined, 1);
}
For more details and a working demonstration, review the User Context sample capsule.
You can use conditionals such as if-else
to specify different endpoints, depending on your capsule's requirements.
...
// conditional remote-endpoint example ("main.url" specified in properties file)
// $vivContext is always passed to remote endpoints
action-endpoint (FindItem) {
if ($vivContext.locale == 'fr_FR') {
remote-endpoint ("{main.url}/fr/item") {
method (GET)
}
}
else {
remote-endpoint ("{main.url}/en/item") {
method (GET)
}
}
}
...
If an API call produces an error (invalid or incomplete inputs, for example), you can return a JSON object with an appropriate HTTP status code, such as a 4xx client error code (for example, 400 Bad Request
). In cases where there are server-side errors, the server returns a 5xx status code such as 500 Internal Server Error
. Generally, status codes other than 200 OK
are considered failures by the platform and will be thrown.
The JSON object returned should correspond to the same format for checked errors that you define in your local function. For example, you might return a JSON error response like this:
{
$type: "checkedError",
$errorId: "NotFound",
$message: "item not found",
$errorObj: { message: "The request item is not available" }
}
$type
: the error type being thrown; for most errors, keep this "checkedError"
$errorId
: the ID of the error as defined in the action$message
: an internal message not displayed to the user$errorObj
: an optional JSON object returned to the actionThe $errorObj
returned in this example is a simple key-value pair with a message shown to the user. You could also return a more complex object for the error handler, or even an existing capsule model.
The above sample corresponds to the checked error defined in the action, as shown here:
action (FindItem) {
type (search)
collect {
...
}
output (Item) {
throws {
error (NotFound) {
property (message) {
type (Message)
min (Required) max (One)
}
on-catch {
halt {
dialog {
template ("Sorry! #{value(message)}.")
}
}
}
}
}
}
}
The error handler above executes a halt
effect, which stops execution. Your error handlers can use any effect in error handling, such as dropping an input and continuing execution with drop
, or specifying a new intent and goal with replan
. For example, a resetOrder
action might create a new empty order by first throwing an error in its JavaScript code:
import console from 'console'
import fail from 'fail'
export function resetOrder(input) {
const { order } = input
console.log('[Store] resetOrder...')
throw fail.checkedError(
// checkedError is the error type
'Reset all data', // error message
'ResetAllData', // errorId
null,
) // no errorObj
return order
}
Then in the corresponding action, replan
creates a new order:
action (ResetOrder) {
type (Constructor)
description (Reset all data includes cart and store)
collect {
input (order) {
type (Order)
min (Optional) max (One)
default-select {
with-rule {
select-first
}
}
}
}
output (Order) {
throws {
error (ResetAllData) {
on-catch {
replan {
intent {
goal { CreateOrder }
}
}
}
}
}
}
}
For more detailed examples of error handling in action, download and read the documentation for the error handling sample capsule.
The following video tutorial shows how to handle errors with the Input Validation and Error Handling sample capsule.
If your endpoint (local, remote, or client) requires OAuth authorization, you can specify the authorization in the authorization.bxb
file and include the authorization
key while specifying the correct scope (either User
or Global
). If the shoe-finding example used OAuth, the authorization
block might look like this:
authorization {
user {
oauth2-authorization-code (Shoebox) {
authorize-endpoint ("{remote.url}/authorize")
token-endpoint ("{remote.url}/token")
client-id (findshoes-shoebox-client)
client-secret-key (remote.key)
}
}
}
The remote endpoint is associated with this authorization by including the authorization { user }
key:
endpoints {
action-endpoints {
action-endpoint (FindShoeRemoteEndpoint) {
accepted-inputs ()
remote-endpoint ("{remote.url}/shoes") {
method (GET)
}
authorization { user }
}
}
}
You might get an OAuth authorization failure if the token is invalid, or the user has manually revoked access through their provider. To handle authorization failures with local endpoints, your JavaScript implementation can throw the built-in authorization-error
type to make Bixby ask the user to log in again.
import console from 'console'
export function getData(input) {
const { search, $vivContext } = input
const endpoint = 'https://data.example.com/feed/'
const query = {
token: $vivContext.accessToken,
searchKeys: search,
}
const response = http.oauthGetUrl(endpoint, {
format: 'json',
query: query,
})
if (response.status == 200) {
return response.parsed
} else if (response.status == 401) {
let err = new Error('Authorization failure')
err.$type = 'authorization-error'
err.$message = 'Authorization failure'
throw err
} else {
console.error('Request failed:', response)
return null
}
}
An endpoint might require one or more types of permissions to be granted by the user in order to execute the action. These actions must specify the required-permissions
key.
action-endpoint (MyAction) {
local-endpoint (MyAction.js)
required-permissions {
permission (contacts)
}
}