Creating Actions
For your code to execute as an action on Adobe I/O Runtime, your code has to comply with two rules:
- You either call your function main or you export the entry point as main. This is the function that will be executed when it is invoked.
- Your function accepts valid JSON objects as input and produces valid JSON objects as output, if needed
You have to configure aio CLI on your machine to create and invoke actions. Refer to the aio CLI page for how to install and configure it.
Let’s assume you have this function available on your machine:
Copied to your clipboard// this is saved in a file named first-function.jsfunction main(params) {var nm = params.name || 'stranger';return {payload: 'Hello ' + nm};}exports.main = main;
You can create an action called test using this command:
Copied to your clipboardaio rt:action:create test first-function.js
You can update an action at any time using the following command:
Copied to your clipboardaio rt:action:update test first-function.js
If you don’t need an action anymore, you can delete it:
Copied to your clipboardaio rt:action:delete test
If you want to save an action that is deployed to your machine, then you can use this command:
Copied to your clipboardaio rt:action:get test --save
Listing all the available actions in your current namespace is as simple as running this command:
Copied to your clipboardaio rt:action:list
Invoking actions
Now, that you have an action, you can call it using the following command (in this example the action name is test):
Copied to your clipboardaio rt:action:invoke test --result
Note the flag --result used in the command. This flag outputs the result of the invocation. Without it, instead of seeing the result of the invocation, you’d get the activation ID. To get the result, you’d use this ID to retrieve the result like this:
Copied to your clipboardaio rt:activation:get <activation ID>
When you invoke an action, as in the example above, the invocation is not blocking (it is async). If you want to execute it in a blocking style and, as a result, get the activation record instead of just getting an activation ID, you have to add the --blocking flag to the command:
Copied to your clipboardaio rt:action:invoke test --blocking
Working with parameters
Actions can receive parameters when are being executed. First, any parameters you sent to the action will be available through the params variable. If you send two parameters called first-name and last-name, they will be available as params.first-name and params.last-name.
Second, let’s see how you can invoke the action with parameters. Our function sample from above uses a parameter called name. This is how you can set the parameter when invoking the action:
Copied to your clipboardaio rt:action:invoke test --param name "John Doe" --result
Setting default parameters
Sometimes you want to bind the same parameter values for all invocations or you just want to set default values. In either case, you have two different options: setting params at the package level (so all actions in that package can inherit them) or at action level.
Default params and encryption
Before we dive deeper in how to set and use default params, let’s discuss the security aspect first. Many developers use the default params as a mechanism to provision actions with the secrets/passwords needed to authenticate against some other systems (databases, services, APIs).
In order to support this use case, all default params are automatically encrypted. They are decrypted just before the action code is executed. Thus, the only time you have access to the decrypted value is while executing the action code.
If you run the CLI command for getting an action or package, you’d get a listing for the names of the default params while the values will be listed as a hash instead of the actual value.
Default parameters set on action
Let’s assume that you want the default value of your parameter to be "Runtime". You can set this value when creating the action, or if the action already exists, updating the action. In both cases you add the *--param" flag:
Copied to your clipboard// creation timeaio rt:action:create test first-function.js --param name "Runtime"// updateaio rt:action:update test first-function.js --param name "Runtime"
Now, you can run the action without any parameters, and it will use the default one you’ve set. If you set a parameter when invoking, this will overwrite the default one.
Important! When updating an action's params using
--param <key> <value>
as above, you must specify ALL of the parameters. All previous values will be overwritten.
Default parameters set on package
When you create an action, it is always created in a package. If you don’t specify a package, the default package is used. Similar to how you set default parameters at the action level, you can specify default parameters at the package level.
The difference is that the params set at the package level will be used for all the actions you create in that package.
Copied to your clipboard// creation timeaio rt:package:create my-package --param name "Runtime"// updateaio rt:package:update my-package --param name "Runtime"
At this point, you might ask yourself what is the precedence when setting parameters at the package level, action level, and invocation time. The winner is parameters set at invocation time, followed by parameters set at action level, and the last is parameters set at package level.
Default parameters with parameter file
A neat way for setting default params is using a dedicated file for storing their values, and then using this file to set those values as default parameter values.
This is especially useful when you are dealing with multiple parameter values for things like configuring API access keys, endpoints, and so on.
Coming back to our sample function that expects one parameter, name, you’d create a JSON file to store that value:
Copied to your clipboard// filename is my-params.json{"name": "Runtime"}
Then, you use the --param-file flag when creating actions, creating packages, or invoking actions.
Copied to your clipboard// update actionaio rt:action:update test first-function.js --param-file my-params.json// invoke actionaio rt:action:invoke test --param-file my-params.json
Final Parameters
Sometimes, an application needs to ensure that the default parameters are final (or immutable), and a calling client can’t override them. You can achieve this by adding the final
annotation - -a final true
:
Copied to your clipboardaio rt:action:update test first-function.js --web true --param name "Runtime" -a final true
This mechanism works for all type of actions including web actions.
Invoking web actions
So far, we’ve been invoking actions only from the CLI. While this might be good for trying out your actions, as you design actions for production systems, you might need to be able to invoke actions via HTTP REST calls. This would enable you to invoke the actions from your web application.
You create a web action by adding the --web flag to the aio
action command:
Copied to your clipboard// creation timeaio rt:action:create test first-function.js --web true// updateaio rt:action:update test first-function.js --web true
Notice the true value used in the command. If you set that value to false, then you disable a web action.
To call the action as a web action, you need to know the full path to the action. You can find the path by adding the --url flag to the action command:
Copied to your clipboardaio rt:action:get test --url
This will give you something like:
Copied to your clipboardok: got action testhttps://adobeioruntime.net/api/v1/web/[your namespace]/default/test
In the URL above, the default
in the path stands for the default
package: if you don’t create your actions explicitly in a package, then they get under the default
package.
You can invoke the action like this:
Copied to your clipboardcurl -L https://adobeioruntime.net/api/v1/web/[your namespace]/default/test -X GET
or
Copied to your clipboardcurl https://[your namespace].adobeioruntime.net/api/v1/web/default/test -X GET
Note the change in the URL here in comparison to what the aio
returns. This is due some additional protections Runtime provides to segregate namespaces from each other when invoking web actions. The aio
generated link will still work but it will return a 308 redirect to your namespace's subdomain on Runtime. For a further discussion of this please see the Securing Web Actions page.
Successful response
When creating actions to be used as web actions, you might want to send the response that follows the HTTP response structure (status code, headers, body). For example, our sample function could be rewritten:
Copied to your clipboardfunction main(params) {var nm = params.name || 'stranger';return {statusCode: 200,body: {payload: 'Hello ' + nm}}}exports.main = main;
You can also set cookies or cache control headers, perform a HTTP redirect, and so forth.
Unsuccessful response
On failed action invocations, the error code and message should be wrapped in an error
object, as this would allow the system to interpret the response as an applicationError
.
Copied to your clipboardasync function main(params) {try {throw new Error("Boom!")} catch (err) {return {error: {statusCode: 500,body: {payload: `Something went wrong: ${err}`}}}}}exports.main = main;
Dealing with errors in general
It is extremely important to handle errors in your action code. It is actually impossible to recover from an unhandled async error in Node.js. Please refer to the Node.js documentation related to uncaughtExceptions.
In the event of an unhandled async errors, the action will be terminated and the container running that action will be destroyed. This means that all in flight activations will be failed and, the next invocation of the action will incur the overhead of creating a new container.
The following examples will show incorrect and correct handling of async errors:
Incorrect handling of async errors
Copied to your clipboardfunction doSomethingAsync() {return new Promise((resolve, reject) => {try {setTimeout(() => {new Error("Something went wrong");}, 1000);} catch (err) {reject(err);}});}
This code will fail to handle the error and the action will be terminated; here the error is generated not while the executor is running, but later. So the try catch block will not be able to catch the error.
Correct handling of async errors
Copied to your clipboardfunction doSomethingAsync() {return new Promise((resolve, reject) => {setTimeout(() => {reject(new Error("Something went wrong"));}, 1000);});}
This code will execute correctly and the error will be handled.
In general when an asynchronous operation is performed, there is a chance that something could go wrong, such as a network error, a database connection issue, or an unexpected input. If an error occurs but is not handled, the node process will be terminated. Therefore, it is important to always handle errors inside the callback function passed to setTimeout or any other async function to ensure the reliability and stability of the program.
HTTP context
When executing an action as a web action, the params
object is decorated with additional information:
Method | Description |
---|---|
__ow_method | the HTTP method of the request |
__ow_headers | the request headers |
__ow_path | the unmatched path of the request (matching stops after consuming the action extension) |
__ow_body | the request body entity, as a base64-encoded string when its content is binary or JSON object/array, or as a plain string otherwise |
__ow_query | the query parameters from the request as an unparsed string |
By default, web actions are accessible to anyone who knows the URL. If you want to secure the access, you can find more info on the Securing Web Actions page.
Deploying ZIP actions
So far, we’ve been creating actions from a single source file. What if you need to create an action from multiple files and you also need some npm modules? The answer is deploying a ZIP action.
You need to do three things to deploy a ZIP action: create a manifest file, create the package.json file, and configure/use wskdeploy
instead of aio
for the actual deploy.
wskdeploy
If you need to install wskdeploy
, check the page Setting up the wskdeploy CLI.
Package.json file
You use the package.json file to specify the dependencies. If you don’t have one already, in the same folder where your function files are, run npm init -y
.
Make sure you have the npm dependencies you need for your functions declared in the package file.
wskdeploy manifest file
The last step is creating the manifest file that will be used by wskdeploy to create your action. In the parent folder of the folder where you have your function files and npm modules, create a file manifest.yaml
. (For more information on YAML format, see the YAML reference card.)
This is the folder structure:
Copied to your clipboard--/actions--/actions/node_modules/--/actions/index.js--/actions/package.json--manifest.yaml
Here is an example manifest.yaml:
Copied to your clipboardpackages:# this is the package nametest:actions:# name of the actiontest-zip:# source for the action; in this case it is a folderfunction: actionsruntime: nodejs:10# publish the action as a web actionweb: yes
Note: If your action require dependencies (such as other node modules or JavaScript files), you need to place the action code in a
index.js
file and in themanifest.yaml
file, thefunction
value should point to the folder where the action is stored instead of the action file. You can see this in action in the example above.
Now you are ready to deploy by running the wskdeploy
command from the same folder where manifest.yaml
is:
Copied to your clipboardwskdeploy
This should deploy an action called test-zip under a package called test. You can invoke it like this:
Copied to your clipboardaio rt:action:invoke test/test-zip --result
If you want to remove this action, you run wskdeploy
with the undeploy flag:
Copied to your clipboardwskdeploy undeploy
There are other useful flows wskdeploy
supports. Please check the official documentation if you want to find more.
Note: There is another way of deploying a ZIP action using the aio command. You miss the manifest.yaml file flexibility with this mode; you can’t define multiple actions/packages at the same time. The ZIP file has to have in the root the package.json file.
aio rt:action:create my-action --kind nodejs:20 zip-file.zip