Functions are the implementations of actions. They actually execute the steps of a plan, by making computations or contacting external APIs. You first define inputs and outputs within an action model first. You then implement functions using JavaScript to provide the necessary logic, operations, and to specify the same inputs and outputs as the action. Local JavaScript is executed in the cloud on Bixby servers, while remote JavaScript is executed on your own server.
This guide describes Version 2 of Bixby's JavaScript Runtime System. The previous system, Version1, is now deprecated. If you still need to migrate an old capsule, read the JavaScript Runtime Version 2 Migration Guide.
For information on the security enhancements made to JavaScript Runtime version 2, see The Bixby Serverless Execution Infrastructure whitepaper.
Bixby's server-side JavaScript environment is built on Google V8, the engine that powers Node.js, and supports JavaScript features through ES2020.
Functions receive a single parameter, an object with key/value pairs whose keys correspond to the input
keys in the action
. You can use destructuring assignment to assign variables from these keys.
Concept model primitives are mapped to and from corresponding JavaScript types. Numeric types are mapped to JavaScript numbers, and string types are mapped to strings. Structures are mapped to JavaScript objects, with property
names as keys. Values defined with multiple cardinality (max (Many)
) will be passed as a JavaScript array. For optional arguments, you can either specify them with values or leave them undefined
with no value in JavaScript.
You could also decide that a default value is not necessary, in which case the value would be null
.
Here's an example of two JavaScript functions that are part of a CreateOrder
action. The first default
function accepts the input parameters from the action input
and returns a JSON object for the output
. The second function, calculateTotalPrice
, calculates the total price of a shirt order. Its input is an array of Item
concepts that include quantity
and price
properties; its output is a totalPrice
structure that includes both the total and a currency type.
import ZonedDateTime from './lib/zoned-date-time-polyfill.js';
import config from 'config';
//CreateOrder
export default function ({ initialItems, $vivContext }) {
ZonedDateTime.setVivContext($vivContext);
return {
items: initialItems.items,
totalPrice: calculateTotalPrice(initialItems.items),
orderNumber: 23343,
holdTime: ZonedDateTime.now('UTC')
.plusSeconds(parseInt(config.get('hold_time')))
.getMillisFromEpoch(),
};
}
function calculateTotalPrice(items) {
var totalPrice = 0;
for (var i = 0; i < items.length; i++) {
totalPrice += items[i].shirt.price.value * items[i].quantity;
}
return {
value: totalPrice,
currencyType: {
prefixSymbol: '$',
currencyCode: 'USD',
},
};
}
Most of the time you can use the parameters passed to your JavaScript functions as expected, as seen in the example above: numbers behave as numbers, strings behave as strings, and so on.
However, these parameters are not the types they appear to be. They are JavaScript objects with additional properties used by Bixby's back end. This means that for comparison operations, you should explicitly cast a parameter to the expected type, or use a comparison operator that performs type coercion.
export default function (input) {
const { searchTerm, $vivContext } = input
if (searchTerm == 'banana') {
// this works, because the `==` operator implicitly performs type
// coercion.
}
if (searchTerm === 'banana') {
// this will NOT work, because the '===' operator requires the types
// to match as well!
}
// the 'swtich' statement should explicitly cast the parameter to the
// type of the 'case', or the case match will fail!
switch (String(searchTerm)) {
case 'banana':
}
}
In some functions, you might need to pass in information about the user, such as user ID, timezone, or locale. In cases like these, you can use the $vivContext
parameter to access user-specific information. This is especially useful for capsules that support multiple locales and customize the response based on the user's locale.
Some $vivContext
variables meant to pass user context information, including deviceModel
and storeCountry
, do not apply to the Simulator.
Learn more about the $vivContext
parameter, and get examples on usage, with the User Context Sample Capsule. Other sample capsules that use $vivContext
include Share Via and User Persistence.
Here is the available data from $vivContext
:
accessToken
: OAuth access token (used with Endpoints).bixbyUserId
: A secure user ID that can be used to persist user state or implement analytics. This ID is tied to the user's account and will be the same across all devices. To access $vivContext.bixbyUserId
, you must request the bixby-user-id-access
permission. See permissions
for more details.
canTypeId
: The CAN type ("target") in which the execution is taking place. Example: bixby-mobile-en-US
.device
: The device class ID. Examples include bixby-mobile
and bixby-tv
. This property is useful, for example, if you are developing for multiple devices and need to display different content depending on device
.deviceModel
: The specific device model of the client. Example: SM-G965N
.grantedPermissions
: Dictionary of permissions granted by the user for the top-level capsule.handsFree
: Boolean indicating whether the user is using the device in a hands-free mode. is24HourFormat
: Boolean indicating whether the device is set to display time in 24-hour format (true
) or AM/PM format (false
).locale
: IETF BCP 47 language tag. Example: en-US
.networkCountry
: The country of the device's current network, denoted in ISO 3166-1 alpha-2. Example: US
or UK
.sessionId
: Unique identifier for a specific conversation by a specific user. You can pass the sessionId
through your external system to track that different requests are from the same conversation by the same user.storeCountry
: The store country specified by the client, denoted in ISO 3166-1 alpha-2. Example: US
or UK
.testToday
: The time in milliseconds since January 1, 1970 set by the Simulator using the GPS/Clock Override section within User settings. Note that testToday
is never actually present in a request from an actual Bixby-enabled device.timezone
: Time zone as per tz database. Example: America/New_York
.Using $vivContext
is particularly useful for choosing specific content or information depending on the target (locale
and/or device
). Here is an example from the Space Resorts sample capsule of using $vivContext
to get the locale
.
In Space Resorts, the MakeReservation
action model simply takes an Order
for a reservation and outputs a Receipt
after the reservation is made.
action (MakeReservation) {
description (Commit order)
type (Commit)
confirm {
by (Confirmation)
}
collect {
input (order) {
type (Order)
min (Required)
}
}
output (Receipt)
}
The JavaScript implementation uses $vivContext
to complete the reservation request, accessing the locale
property to determine "relevant hours" as set in the capsule properties file (accessed with config.get()
in the code below).
import ZonedDateTime from "../lib/zoned-date-time-polyfill.js";
import config from 'config';
import console from 'console';
export default function ({order, $vivContext}) {
var relevantHours = getLocalizedRelevantHours($vivContext);
console.info($vivContext.locale, 'relevant hours', relevantHours);
ZonedDateTime.setVivContext($vivContext);
return {
$id: '' + Math.floor(Math.random() * 10000000),
item: order.item,
buyer: order.buyer,
relevantDateTime: ZonedDateTime.fromDate(
order.item.dateInterval.start
)
.minusHours(relevantHours)
.getDateTime(),
};
};
function getLocalizedRelevantHours($vivContext) {
switch ($vivContext.locale.split('-')[0]) {
case 'ko':
return config.get('ko.relevantHours');
default:
return config.get('base.relevantHours');
}
}
The local endpoint for the MakeReservation
action, defined in resources/base/book.endpoints.bxb
, includes $vivContext
as one of its accepted-inputs
.
action-endpoint (MakeReservation) {
accepted-inputs (order, $vivContext)
local-endpoint (book/MakeReservation.js)
}
As with the other function parameters, Bixby assigns values based on the parameter variable names, so you must use $vivContext
as the parameter name to receive the context information.
When you need to target a specific device, you can use $vivContext.device
and conditionals to choose the content for Views. For example, if you are developing for a watch, it can take longer to load larger images. To choose a lower resolution image on a watch, use an if/else
statement to switch the image URL depending on whether $vivContext.device
is bixby-mobile
or bixby-watch
.
The following video tutorial describes several ways to use the $vivContext
variable with the user-context
sample capsule.
Context between capsules is not always stateful, meaning that if the user leaves the capsule for another capsule, or if the user leaves Bixby altogether, then any context from the first capsule will not be remembered. Capsules also have a specific capsule lock-in time when dealing with prompts. Capsule lock-in remembers which capsule users are in for ten seconds after they respond to a prompt.
If your capsule does need to remember context between multiple conversations, you can use remote endpoints and set up your service how you want. To store the state, use remote points to store as much information as needed on your own server.
If you need to persist data for multiple users across conversations with a remote database, you can read about the User Persistance sample capsule.
Things can sometimes go wrong inside a function. You should anticipate what might go wrong and use input validation to catch potential errors. But if the error occurs within JavaScript code—when computations are necessary to determine input validity, for example, or validity is based on the results of an external service—you can use the fail
JavaScript library to throw a checked error.
In the Space Resorts capsule, an error is thrown when multiple pods are matched by the SelectPod
action.
if (matches.length > 1) {
throw fail.checkedError(
'Multiple habitat pods matched',
'MultipleMatches',
{ matches: matches }
);
}
The value of the first parameter ("Multiple habitat pods matched") is logged by Bixby, but users won't see it; you can use it for debugging. You should write any user-visible dialog as part of the model. The second parameter (MultipleMatches
) is the name of the error to match with the error
key in the handler's on-catch
block. Use the object in the third parameter ({matches: matches}
) to pass any expected properties to the action model's error handler declaration. In the SelectPod
action, this handler uses the replan
effect to prompt the user to choose one of the matched pods.
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) }
}
}
}
}
}
}
When testing your function, you can expect to see details of any errors thrown by functions.
For more information and examples, read about Error Handling.
Within functions, you can add API calls for both SOAP and REST-based web services.
To call a REST-based web service, use one of these calls:
http.getUrl(url, options)
http.postUrl(url, params, options)
http.putUrl(url, params, options)
http.deleteUrl(url, params, options)
In each case, url
is the URL of the service.
The options
argument is a JSON object that can contain the following parameters:
format
: This specifies how the response will be processed. It can be one of 'text', 'json', 'xmljs', or 'csv'.query
: An object with attribute/value pairs to pass as arguments to the REST service.basicAuth
: HTTP Basic Authentication. The value must be an object with "username" and "password".cacheTime
: Cache time in milliseconds. By default, all GET requests are cached in memory. An alternate cache time can be provided.headers
: An object with key/value pairs for additional HTTP headers. This can be used for custom Content-Type
settings (such as XML).passAsJson
(POST call only): If set to true
, passes the request parameters in the body in JSON format and sets the content type appropriately.returnHeaders
: If set to true
, returns HTTP headers and status code. This also suppresses exceptions on HTTP error status codes, leaving this to the developer to handle.Here's an example from the HTTP API Calls sample capsule:
var http = require('http')
var console = require('console')
module.exports.function = function findShoe (type) {
console.log("FindShoe filter by a specific type")
var options = {
format: 'json',
query: {
type: type
}
};
// makes a GET call to /shoes?type=Formal
var response = http.getUrl('https://my-json-server.typicode.com/bixbydevelopers/capsule-samples-collection/shoes', options);
return response;
}
The library makes an HTTP request (with a timeout, to prevent excessive server resource usage), and processes the response according to the format specifier. Each format specifier maps the response to a JavaScript data structure. The text
format, for example, returns a string and is useful for text, or any response where you wish to do the parsing manually in JavaScript. The JSON format maps to JavaScript objects in a straightforward way.
The xmljs
format maps an XML response to JSON using StAXON conventions.
The result can then be accessed as a JavaScript object. For example, consider this XML response:
<reviews>
<review id="166739">
<content>Great!...</content>
<reviewer>Teschia</reviewer>
<star-rating>4</star-rating>
<order-count>92</order-count>
<review-count>31</review-count>
</review>
<review>
...
</review>
<reviews>
The above would map to this JSON structure:
{
"reviews" : {
"review" : [
{
"@id" : "166739",
"content" : "Great!...",
"reviewer" : "Teschia",
"star-rating" : "4",
"order-count" : "92",
"review-count" : "31"
},
]
}
}
So it is then possible to access fields via expressions like response.reviews.review[0].content
.
For XML and JSON responses, you should use a construct similar to the following to verify that a document was parsed and has the expected structure:
const content = reviews.review && reviews.review[0] && reviews.review[0].content
In addition, the XML-to-JSON mapping can return single values or arrays, depending on whether there is one element or a sequence of elements of the same name. If you know a variable should be an array, even if there is only one element, you can write something like:
let reviewsList = [].concat(response.review)
The CSV format maps a CSV-formatted result to an array of arrays.
For a POST call, params
, within the http.postUrl(url, params, options)
syntax,
includes the parameters to pass as the body of the request. Normally, it should be an object with the desired keys and values, and these will be HTTP form encoded. However, if a different format for the body is required, it is possible to use passAsJson
(for JSON-formatted bodies), or a fully custom format (such as XML) by passing a string instead of an object. In the latter case, you might want to set the Content-Type
in the headers
option.
Here's an example JavaScript action that uses http.postUrl
:
var http = require('http')
var console = require('console')
module.exports.function = function createShoe () {
var shoe = {
"name": "Test Shoes",
"description": "Test shoes that get POSTed to the server. They will not persist.",
"price": {
"value": 65,
"currencyType": {
"currencyCode": "USD",
"prefixSymbol": "$"
}
},
"type": "Boot"
};
var options = {
passAsJson: true,
returnHeaders: true,
format: 'json'
};
var response = http.postUrl('https://my-json-server.typicode.com/bixbydevelopers/capsule-samples-collection/shoes', shoe, options);
console.log(response);
return response.parsed;
}
For a DELETE call, you would modify the above code to use http.deleteUrl
instead of http.postUrl
.
Learn more about HTTP API Calls in Sample Capsule documentation.
By default, if the status code is any HTTP error code (less than 200 or 4xx or 5xx), an exception is thrown. See Throwing Exceptions for more information on how to handle exceptions.
Remote endpoints must return CheckedError
objects on errors. If an existing REST API is being called, use a local endpoint to handle errors and throw exceptions.
However, if you enable the returnHeaders
option, this behavior is disabled, and you will get an object containing details of the returned content, including these fields:
status
is the HTTP status codeheaders
contains the HTTP headersresponseText
contains the raw HTTP response body, as a stringparsed
contains the parsed response (depending on the format
option)If your capsule needs to communicate with a web service via SOAP, you can use template literals to create the XML payload and send it to the remote server using http.postUrl()
. By sending it with the xmljs
format parameter, the returned XML will be translated into JSON.
import http from 'http'
import config from 'config'
export function handler(input) {
const options = {
format: 'xmljs',
returnHeaders: true,
}
const soapMessage = `
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://...">
<soapenv:Header/>
<soapenv:Body>
<ord:order>
<ord:firstName>${recipient.firstName}</ord:firstName>
<ord:lastName>${recipient.lastName}</ord:lastName>
<ord:address>${recipient.address}</ord:address>
<ord:deliveryDate>${deliveryDateStr}</ord:deliveryDate>
<ord:product>
<ord:sku>${product.sku}</ord:sku>
<ord:quantity>1</ord:quantity>
</ord:product>
<ord:totalAmount>${charges}</ord:totalAmount>
</ord:orders>
</soapenv:Body>
</soapenv:Envelope>`
const response = http.postUrl(config.get('remote.url'), soapMessage, options)
return response
}
As described in Authorizing Access with OAuth, Bixby manages the different developer keys needed. The function script must simply reference the correct URL and required parameters for user authorization.
Here is an example function that uses OAuth to access a user's Google contacts:
export default function (input) {
const { searchTerm } = input;
const response = http.oauthGetUrl(
// the URL
"https://www.google.com/m8/feeds/contacts/default/full",
// options
{
query: { q: searchTerm },
// Special header required by provider:
headers: { "GData-Version": "3.0" },
format: 'xmljs',
}
)
const contactInfos = // Extract data here
return contactInfos
}
const authorization = {
'provider': 'google',
'scope': 'https://www.google.com/m8/feeds'
};
Note that the function exports an authorization
section that specifies the kind of authorization needed. You must include a 'provider'
declaration. The provider registration specifies all details needed, such as the OAuth version required by that provider, and other details. You can only use OAuth 2.0. The 'scope'
field is optional, but can be supplied to specify access to a particular set of services with the provider.
If a function has a valid authorization
section, it can use the OAuth methods of the http
package, as shown above. These methods operate in the same manner as regular http
package tools, such as http.getUrl()
and http.postUrl()
, but automatically attach the Authorization header with the appropriate access key. All format handling and other usage is the same as with regular non-OAuth services. Here is a complete list of the OAuth methods in the http
package:
http.oauthGetUrl()
- same as http.getUrl()
http.oauthDeleteUrl()
- same as http.deleteUrl()
http.oauthPostUrl()
- same as http.postUrl()
http.oauthPutUrl()
- same as http.putUrl()
When users make use of any of these functions the first time, they must authorize the function to access their data at the given provider. If they confirm, Bixby stores the access keys for later invocations.
As part of the OAuth flow, you are required to register a redirect URI (redirect_uri
) with the service provider. Normally, this is set in the service provider's dashboard. Refer to your provider's documentation for more information.
Register Bixby's redirect URI to the following:
https://<your-capsule-id>.oauth.aibixby.com/auth/external/cb
Replace <your-capsule-id>
with your encoded capsule ID, with the -
dash character in place of the .
period character. For example, if your capsule ID is example.dice
, your redirect URI would be https://example-dice.oauth.aibixby.com/auth/external/cb
.
Many external APIs require developer keys. While some settings for accessing external services might be stored directly in JavaScript configurations, these access keys must be protected to prevent inadvertent disclosure.
Developer keys and other sensitive information must be stored as secrets in the Configuration & Secrets section for that capsule in the Bixby Developer Center. You should never store keys and other credentials in your capsule's source code.
You can list URLs and non-sensitive details in the capsule.properties
file, which you should place in the resources/base
folder.
This is an example of a capsule.properties
file:
## Set the config mode of this capsule
# capsule.config.mode=default
## Set configuration properties with `config.<mode>.<propertyKey>=<propertyValue>
## Use mode `default` for fallback property values
# config.default.my.property.key=myValue 123
## To prevent any content in this capsule from loading, set `capsule.enabled` to `false`
# capsule.enabled=false
config.default.oauth.url=https://api.example.com/oauth2/token
Once your secrets and properties are entered, you can reference them within your JavaScript code using the config.get
and secret.get
commands.
let authorization = {
provider: 'ReviewAPI',
type: 'OAuth2.0',
grantType: 'client_credentials',
clientId: secret.get('oauth.id'),
clientSecret: secret.get('oauth.secret'),
tokenEndpoint: config.get('oauth.url'),
}
The config.get()
command can access properties defined in both the capsule.properties
file and in the Configuration & Secrets screen in the Bixby Developer Center; you can override a property in capsule.properties
by giving it a new value in the Bixby Developer Center. (For instance, if the token endpoint in the example above changed, you could override the value defined in your capsule with a new value in the Bixby Developer Center. Alternatively, oauth.url
could be defined only in the Bixby Developer Center and not in capsule.properties
at all.) The secret.get()
command only accesses values defined in the Bixby Developer Center; you cannot set secrets within a properties file.
Unlike user data discussed above, developer JavaScript code has full access to the keys within JavaScript. It is your responsibility to avoid inappropriate use of keys.
In addition to the web services modules provided above, Bixby provides a few other libraries for developers to use within their functions. See the JavaScript API documentation for details.
Many concepts have an inherent ID associated with them, such as a business ID or a product ID. These IDs are provider-specific and are only meaningful to the provider who creates and uses them. For instance, a restaurant ordering ID has no functional meaning to other providers other than the restaurant ordering service. For privacy and accounting reasons, IDs are given special handling by Bixby. No modeling at the concept level is required for IDs; if a function provider has an ID it wants to associate with an instance it is generating, it can assign a value to the $id
property at any level of the structured return object. Then later, any instances passed into a function can be tested for the presence of this $id
property. If the ID is associated with the particular function provider, it will be present; otherwise, Bixby filters it out.
// During the mapping phase in within
// restaurant.FindRestaurants.js, we assign a business id to each returned
// restaurant item
const item = {
$id: result.id,
name: result.name, // optional restaurant.RestaurantName
restaurantStyle: categories, // optional restaurant.RestaurantStyle array
photo: result.image ? [{ url: result.image, size: 'medium' }] : undefined,
businessUrl: result.website, // optional entity.EntityUrl
address: address, // optional geo.Address
review: shared.getReviews(result.id), // optional rating.Review array
}