Writing actions
- TypeScript
- JavaScript
- Python
Actions are the foundation of tools built with Interval. Actions are just async functions which means anything you can do within a JavaScript function is possible within an action. And by extension, if you know how to write JavaScript functions, you know how to write Interval actions.
Actions are the foundation of tools built with Interval. Actions are just async functions which means anything you can do within a JavaScript function is possible within an action. And by extension, if you know how to write JavaScript functions, you know how to write Interval actions.
Actions are the foundation of tools built with Interval. Actions are just async coroutines which means anything you can do within a coroutine function is possible within an action. And by extension, if you know how to write coroutine functions, you know how to write Interval actions.
But actions have access to special functionality that other functions don't, namely they can:
- collect input and display output through I/O methods,
- be run by a member of your organization from the Interval dashboard,
- and send push notifications to members of your organization.
Defining actions
Actions are identified by slugs that are defined in your code and are unique to your organization.
The simplest and most common way to define actions is by loading them from
the filesystem using the routesDirectory
property.
When routesDirectory
is defined, Interval will recursively walk directories
and detect files with default exports of Actions or
Pages and create slugs for them based on their files' paths.
tip
Relative path resolution is based on your Node.js process's current working
directory. In order to load starting from a path relative to the Interval
constructor, be sure to provide an explicit path, for instance by using the __dirname
variable and path.resolve
.
The simplest and most common way to define actions is by loading them from
the filesystem using the routesDirectory
property.
When routesDirectory
is defined, Interval will recursively walk directories
and detect files with default exports of Actions or
Pages and create slugs for them based on their files' paths.
tip
Relative path resolution is based on your Node.js process's current working
directory. In order to load starting from a path relative to the Interval
constructor, be sure to provide an explicit path, for instance by using the __dirname
variable and path.resolve
.
The simplest and most common way to define actions is to add the @interval.action
decorator to an async coroutine. If you want to set additional metadata for the action, you can pass options to the decorator specifying metadata properties:
- TypeScript
- JavaScript
- Python
import { Interval } from "@interval/sdk";
import path from "path";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routesDirectory: path.resolve(__dirname, "routes"),
});
interval.listen();
import { Action, io } from "@interval/sdk";
export default new Action(async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
});
import { Action, io } from "@interval/sdk";
export default new Action({
name: "Create user",
description: "Creates a user",
handler: async () => {
// action logic here
},
});
The Action
class constructor accepts either an async handler
function, or an object with the following properties:
handler
: The async function that performs your action logic.name
: The string short display name for the action in the dashboard.description
: A string longer description of the visible on the dashboard index.backgroundable
: A boolean that allows the action to run in the background, either via a schedule or when the runner leaves the page. See Long running actions for more information.unlisted
: A boolean indicating that the action should not be visible in the dashboard, intended to be accessible only by direct link.access
: The access control definition for the action. See Authentication and access control for more information.warnOnClose
: A boolean indicating whether confirmation prompts should be shown to the user when navigating away from an in-progress transaction. Defaults totrue
, set tofalse
to disable.
const { Interval } = require("@interval/sdk");
const path = require("path");
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routesDirectory: path.resolve(__dirname, "routes"),
});
interval.listen();
const { Action, io } = require("@interval/sdk");
module.exports = new Action(async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
});
const { Action, io } = require("@interval/sdk");
module.exports = new Action({
name: "Create user",
description: "Creates a user.",
handler: async () => {
// action logic here
},
});
The Action
class constructor accepts either an async handler
function, or an object with the following properties:
handler
: The async function that performs your action logic.name
: The string short display name for the action in the dashboard.description
: A string longer description of the visible on the dashboard index.backgroundable
: A boolean that allows the action to run in the background, either via a schedule or when the runner leaves the page. See Long running actions for more information.unlisted
: A boolean indicating that the action should not be visible in the dashboard, intended to be accessible only by direct link.access
: The access control definition for the action. See Authentication and access control for more information.
from interval_sdk import Interval, IO
@interval.action
async def refund_user(io: IO):
# action logic here
pass
@interval.action(name="Import data", description="Imports data.", backgroundable=True)
async def import_data(io: IO):
# action logic here
pass
@interval.action(name="Edit user", description="Only accessible via direct link with user ID parameters", unlisted=True)
async def edit_user(io: IO):
# action logic here
pass
interval.listen()
The interval.action
decorator accepts these named arguments:
name
: The string short display name for the action in the dashboard.description
: A string longer description of the visible on the dashboard index.backgroundable
: A boolean that allows the action to run in the background, either via a schedule or when the runner leaves the page. See Long running actions for more information.unlisted
: A boolean indicating that the action should not be visible in the dashboard, intended to be accessible only by direct link.access
: The access control definition for the action. See Authentication and access control for more information.
Defining routes inline
If necessary, can also be defined inline using the routes
property on the
Interval
or Page
constructors:
import { Interval } from "@interval/sdk";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
},
});
interval.listen();
Defining routes inline
If necessary, can also be defined inline using the routes
property on the
Interval
or Page
constructors:
import { Interval } from "@interval/sdk";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
},
});
interval.listen();
Dynamically adding and removing routes
Routes can also be defined dynamically after calling listen()
by using
the interval.routes.add()
method:
- TypeScript
- JavaScript
- Python
interval.listen();
interval.routes.add("export_data", async () => {
// action logic here
});
Routes can be dynamically removed as well, such as to make an action
unavailable in response to some event. You can do so with the
interval.routes.remove()
method:
interval.listen();
interval.routes.remove("export_data");
interval.listen();
interval.routes.add("export_data", async () => {
// action logic here
});
Routes can be dynamically removed as well, such as to make an action
unavailable in response to some event. You can do so with the
interval.routes.remove()
method:
interval.listen();
interval.routes.remove("export_data");
interval.listen()
async def export_data(io: IO):
# action logic here
pass
interval.routes.add("export_data", export_data)
Routes can be dynamically removed as well, such as to make an action
unavailable in response to some event. You can do so with the
interval.routes.remove()
method:
interval.listen();
interval.routes.remove("export_data")
Rendering UIs
- TypeScript
- JavaScript
- Python
Inside of Interval actions, you can use I/O methods to collect input and display output. Interval handles the heavy lifting of rendering the appropriate UIs.
I/O methods can be awaited
just like any other JavaScript promise. The execution of your action handler function will be suspended until the person running the action provides the requested input or acknowledges the data you've displayed.
Inside of Interval actions, you can use I/O methods to collect input and display output. Interval handles the heavy lifting of rendering the appropriate UIs.
I/O methods can be awaited
just like any other JavaScript promise. The execution of your action handler function will be suspended until the person running the action provides the requested input or acknowledges the data you've displayed.
Inside of Interval actions, you can use I/O methods to collect input and display output. Interval handles the heavy lifting of rendering the appropriate UIs.
I/O methods can be awaited
just like any other async coroutine. The execution of your action handler function will be suspended until the person running the action provides the requested input or acknowledges the data you've displayed.
In practice, this means you can collect input and display output as easily as calling any other function. You can focus on your business logic instead of the complexities of UI programming, like managing state, reactivity, etc.
Type safety and validation
Interval's I/O methods are type safe by design. For example, a call to io.input.number will return a primitive JavaScript number that is statically typed by TypeScript.
In addition to basic type safety, Interval will also enforce validation where logical. For example, a call to io.input.email will return a JavaScript string that is guaranteed by Interval to be a valid email address.
Interval's I/O methods are type safe by design. For example, a call to io.input.number will return a primitive JavaScript number that is statically typed by TypeScript.
In addition to basic type safety, Interval will also enforce validation where logical. For example, a call to io.input.email will return a JavaScript string that is guaranteed by Interval to be a valid email address.
Interval's I/O methods are type safe by design. For example, a call to io.input.number will return an int
or float
type from the Python standard library, or a call to io.input.date will return a date object.
In addition to basic type safety, Interval will also enforce validation where logical. For example, a call to io.input.email will return a str
that is guaranteed by Interval to be a valid email address.
A simple example
Imagine you need to collect an email address from the person running your action. In Interval, this can be accomplished with a single line of code.
- TypeScript
- JavaScript
- Python
// email is a string containing an email address
const email = await io.input.email("Enter an email");
Note that I/O methods can only be used inside actions. Here's the above line in context of an Interval action:
import { Action, io } from "@interval/sdk";
export default new Action(async () => {
const email = await io.input.email("Enter an email");
});
// email is a string containing an email address
const email = await io.input.email("Enter an email");
Note that I/O methods can only be used inside actions. Here's the above line in context of an Interval action:
import { Action, io } from "@interval/sdk";
export default new Action(async () => {
const email = await io.input.email("Enter an email");
});
// email is a string containing an email address
email = await io.input.email("Enter an email")
Note that I/O methods can only be used inside actions. Here's the above line in context of a complete Interval app:
import os
from interval_sdk import Interval, IO
interval = Interval(
os.environ["INTERVAL_API_KEY"],
)
@interval.action
async def collect_email(io: IO):
email = await io.input.text("Enter an email")
interval.listen()
Requesting an optional value
By default, when you request input with an I/O method, the person running your action must supply a valid value before continuing. In some cases, you may want to allow the person running you action to skip a field.
This can be done simply by chaining your I/O method call with .optional()
.
For example:
- TypeScript
- JavaScript
- Python
// maybeEmail is a string containing an email address *or* undefined
const maybeEmail = await io.input.email("Enter an email").optional();
// or to conditionally mark as optional
const maybeEmail = await io.input
.email("Enter an email")
.optional(user.email !== null);
// maybeEmail is a string containing an email address *or* undefined
const maybeEmail = await io.input.email("Enter an email").optional();
// or to conditionally mark as optional
const maybeEmail = await io.input
.email("Enter an email")
.optional(user.email !== null);
# maybe_email is a string containing an email address *or* None
maybe_email = await io.input.email("Enter an email").optional()
# or to conditionally mark as optional
maybe_email = await io.input
.email("Enter an email")
.optional(user.email is not None)
Grouping inputs together
When writing actions, you'll often want to call multiple I/O methods at the same time. For example, for a tool to create a user account, you may want to collect an email, name, and age. To do this in Interval, you can use the io.group
method. If you've used Promise.all before, Interval's group method works effectively identically:
When writing actions, you'll often want to call multiple I/O methods at the same time. For example, for a tool to create a user account, you may want to collect an email, name, and age. To do this in Interval, you can use the io.group
method. If you've used Promise.all before, Interval's group method works effectively identically:
When writing actions, you'll often want to call multiple I/O methods at the same time. For example, for a tool to create a user account, you may want to collect an email, name, and age. To do this in Interval, you can use the io.group
method.
- TypeScript
- JavaScript
- Python
const [name, email, age] = await io.group([
io.input.text("Name"),
io.input.email("Email"),
io.input.number("Age"),
]);
const [name, email, age] = await io.group([
io.input.text("Name"),
io.input.email("Email"),
io.input.number("Age"),
]);
name, email, age = await io.group(
io.input.text("Name"),
io.input.email("Email"),
io.input.number("Age"),
);
See io.group for more information.
Customizing the submit button
By default, each "step" within your action that requests user input renders a single submit button allowing the person running the action to move on when they've filled out the form.
To change the button label or add additional buttons, chain any I/O method or group with withChoices()
, and pass an array of options defining the available paths forward. Each provided option will render a button in the action UI, and change the I/O method or group return value to contain a choice
identifying the chosen button in addition to the usual return value(s) from the I/O methods.
To change the button label or add additional buttons, chain any I/O method or group with withChoices()
, and pass an array of options defining the available paths forward. Each provided option will render a button in the action UI, and change the I/O method or group return value to contain a choice
identifying the chosen button in addition to the usual return value(s) from the I/O methods.
To change the button label or add additional buttons, chain any I/O method or group with with_choices()
, and pass an array of options defining the available paths forward. Each provided option will render a button in the action UI, and change the I/O method or group return value to contain a choice
identifying the chosen button in addition to the usual return value(s) from the I/O methods.
- TypeScript
- JavaScript
- Python
const {
choice,
returnValue: { num1, num2 },
} = await io
.group({
num1: io.input.number("First number"),
num2: io.input.number("Second number"),
})
.withChoices(["Add", "Subtract", "Multiply"]);
const {
choice,
returnValue: { num1, num2 },
} = await io
.group({
num1: io.input.number("First number"),
num2: io.input.number("Second number"),
})
.withChoices(["Add", "Subtract", "Multiply"]);
response = await io.group(
num1=io.input.number("First number"),
num2=o.input.number("Second number"),
).with_choices([
"Add",
"Subtract",
"Multiply",
]);
See Submit buttons for more information.
Skipping the completion screen
By default, Interval actions show a completion message when they're finished running. For often-used actions, especially when used with Pages, you may want to skip the completion screen and redirect the user to another screen. To achieve this behavior you can use ctx.redirect at the end of an action.
caution
By default, code after ctx.redirect
in an action handler is not guaranteed to run. If you want to run additional code after redirecting the user away from the screen (e.g. a long-running database operation), enable the backgroundable
setting on the action, which allows it to continue running to completion in the background. Learn more about running actions in the background here.
Here's an example of an action that creates a user and then redirects back to the main 'Users' screen:
- TypeScript
- JavaScript
- Python
import { Action, io, ctx } from "@interval/sdk";
import { createUser } from "../../db";
export default new Action({
name: "Create user",
handler: async () => {
const { name, email } = await io.group({
name: io.input.text("Name"),
email: io.input.text("Email"),
});
await createUser({ name, email });
// skips the completion screen and redirects the user back to the Users screen
await ctx.redirect({ route: "users" });
},
});
import { Action, io, ctx } from "@interval/sdk";
import { createUser } from "../../db";
export default new Action({
name: "Create user",
handler: async () => {
const { name, email } = await io.group({
name: io.input.text("Name"),
email: io.input.text("Email"),
});
await createUser({ name, email });
// skips the completion screen and redirects the user back to the Users screen
await ctx.redirect({ route: "users" });
},
});
import os
from interval_sdk import Interval, IO, ctx_var
import my_db
interval = Interval(
os.environ["INTERVAL_API_KEY"],
)
@interval.action
async def create_user(io: IO):
name, email = await io.group(
io.input.text("Name"),
io.input.email("Email"),
)
await my_db.user.create(name, email)
# skips the completion screen and redirects the user back to the Users screen
ctx = ctx_var.get()
await ctx.redirect(route="users")
interval.listen()
For more on how linking works, see Linking between actions.