Pages
While actions are the bread and butter of building with Interval, Interval's Pages API enables more sophisticated UIs and structure, such as dashboards, list and resource views, and hierarchical navigation.
info
The Pages API is available on SDK versions v0.35.0 and higher.
Pages are still in beta. The API names and functionality may change.
With the Pages API you can:
- Define top-level navigation within your organization's Interval dashboard.
- Group related actions to provide better structure and easier navigation.
- Create screens with text, data, and list views to be used as a launching point for other actions.
The Pages API introduces a second type of screen, called a page. While actions are designed for collecting input and displaying output (analogous to PUT/POST/DELETE in HTTP), pages are just for displaying content (analogous to GET in HTTP). For example, you might create a Users screen in your app, where the root Users screen that shows a list of users is a page, and the Create user and Update user forms are actions.
Together, pages and actions - along with linking APIs for navigating between the two - are the primary building blocks for developing apps with Interval.
Defining pages
To create a page, create a Page
instance and pass it in when you instantiate your Interval
class, alongside any existing actions. This will create a top-level navigation link for the page and provide a space for nesting related actions within the page.
The Page
constructor accepts an object of the following shape:
name
: The page's name. Required.routes
: An object containing the actions and sub-pages nested within the page, just likeInterval
's top-levelroutes
property. Optional.unlisted
: An optional boolean property that hides the page from auto-generated navigation if set totrue
. Defaults tofalse
.
caution
With the introduction of the Pages API, the actions
param in the Interval
class has been renamed to routes
. You can still define actions here as usual, in addition to pages.
- TypeScript
- JavaScript
- Python Experimental
import Interval, { Page, io } 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}`;
},
users: new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
}),
},
});
interval.listen();
import Interval, { Page, io } 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}`;
},
users: new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
}),
},
});
interval.listen();
import os
from interval_sdk import Interval, IO, Page
interval = Interval(
os.environ["INTERVAL_API_KEY"],
)
@interval.action
async def hello_world(io: IO):
name = await io.input.text("Your name")
return f"Hello {name}"
users_page = Page(name="Users")
@users_page.action
async def create(io: IO):
# Usual action code, just nested within a page
return "User created"
interval.routes.add("users", users_page)
interval.listen()
Page
objects can themselves contain other pages, allowing you to construct arbitrary path-like hierarchies. In the above code example, these four paths are generated:
hello_world
users
users/create
You'll use these paths when linking between actions.
tip
Action slugs do not need to be unique across different routes.
In addition to being defined on the Interval
or Page
constructors, routes
can be added or removed dynamically via the add()
and remove()
methods. This works the same as adding and removing actions, and the add()
and remove()
methods are also available on Page
to manage sub-pages or nested actions within a page.
Customizing page layout
The default appearance of a Page
is a grid or list view of the actions defined within it, similar to the root Dashboard screen:
For more control over output - such as displaying text, metadata, and list views - you can define
an asynchronous handler()
function for the Page
which allows for customizing the page's layout.
A Page
's handler is conceptually similar to an Action handler, but it can't await input; it must return an instance of the Layout
class.
Layout
supports the following properties in its class constructor:
title
: The page's title and primary heading. Typestring | Promise<string> | (() => string | Promise<string>)
. Optional. If not defined, it will use thename
property from its parentPage
.description
: The page's description. Typestring | Promise<string> | (() => string | Promise<string>)
. Optional.menuItems
: An array of objects defining links to other actions and populate in the page as a menu of buttons.children
: An array ofio.display
components to render as the page's body. These should not be awaited. Optional.
In the following example, the default actions list is replaced with a list of users:
- TypeScript
- JavaScript
- Python Experimental
import Interval, { Page, Layout, io } from "@interval/sdk";
import { fetchUsers } from "../db/users";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
users: new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
handler: async () => {
const betaUsers = await fetchUsers();
return new Layout({
title: "Beta users",
description: "These users have signed up for the beta.",
menuItems: [
{
label: "Create user",
action: "users/create",
},
],
children: [
io.display.metadata("", {
layout: "card",
data: [
{
label: "Total users",
value: betaUsers.length,
},
{
label: "Active users",
value: betaUsers.filter(u => u.isActive).length,
},
],
}),
io.display.table("Users", {
data: betaUsers,
}),
],
});
},
}),
},
});
interval.listen();
import Interval, { Page, Layout, io } from "@interval/sdk";
import { fetchUsers } from "../db/users";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
users: new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
handler: async () => {
const betaUsers = await fetchUsers();
return new Layout({
title: "Beta users",
description: "These users have signed up for the beta.",
menuItems: [
{
label: "Create user",
action: "users/create",
},
],
children: [
io.display.metadata("", {
layout: "card",
data: [
{
label: "Total users",
value: betaUsers.length,
},
{
label: "Active users",
value: betaUsers.filter(u => u.isActive).length,
},
],
}),
io.display.table("Users", {
data: betaUsers,
}),
],
});
},
}),
},
});
interval.listen();
import os
from interval_sdk import Interval, IO, Page, Layout
from db.users import fetch_users
interval = Interval(
os.environ["INTERVAL_API_KEY"],
)
@interval.action
async def hello_world(io: IO):
name = await io.input.text("Your name")
return f"Hello {name}"
users_page = Page(name="Users")
@users_page.handle
async def handler(display: IO.Display):
beta_users = await fetch_users()
return Layout(
title="Beta users",
description="These users have signed up for the beta.",
menu_items=[
{"label": "Create user", "route": "users/create"},
],
children=[
display.metatdata(
"",
layout="card",
data=[
{"label": "Total users", "value": len(beta_users)},
{"label": "Active users", "value": len([u for u in beta_users if u.is_active])},
]
),
display.table("Users", data=beta_users),
],
)
@users_page.action
async def create(io: IO):
# Usual action code, just nested within a page
return "User created"
interval.routes.add("users", users_page)
interval.listen()

Loading routes from the filesystem
caution
This feature is currently only available in the TypeScript/JavaScript SDK. If you're interested in filesystem routes with Python, let us know!
As the number of actions and pages in your app grows, you'll likely want to move them
out of the Interval
constructor into separate files. Loading actions and pages from
the filesystem can be done automatically with the routesDirectory
property:
import Interval from "@interval/sdk";
import path from "path";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routesDirectory: path.resolve(__dirname, "routes"),
});
When routesDirectory
is defined, Interval will recursively walk directories
and detect files with default exports of Action
s or Page
s and add them
to your routes
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.
A Page
is automatically created for each directory, and files inside the directory that export
an Action
or Page
will be nested within that Page
. The directory's auto-generated Page
can be customized by
an index.ts
(or index.js
) file, which should export a Page
as its default export.
Using the Users example above:
index.ts
defines theInterval
constructor and configuresroutesDirectory
users/index.ts
exports aPage
as its default exportusers/create.ts
exports anAction
as its default export
Therefore, these two examples are equivalent:
- Using routesDirectory
- Using routes
// in `src/interval.ts`
import Interval from "@interval/sdk";
import path from "path";
const interval = new Interval({
apiKey: "<YOUR API KEY>",
routesDirectory: path.resolve(__dirname, "routes"),
});
interval.listen();
// in `src/routes/hello_world.ts`
import { Action, io } from "@interval/sdk";
export default new Action(async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
});
// in `src/routes/users/index.ts`
import { Page, Layout, io } from "@interval/sdk";
export default new Page({
name: "Users",
handler: async () => {
return new Layout({
title: "Beta users",
description: "These users have signed up for the beta.",
metadata: [
{
label: "Total users",
value: 2,
},
{
label: "Active users",
value: 1,
},
],
menuItems: [
{
label: "Create user",
action: "users/create",
},
{
label: "View metrics",
action: "hello_world",
},
],
children: [
io.display.table("Users", {
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bartholomew", email: "bart@example.com" },
],
}),
],
});
},
routes: {
edit: {
name: "Edit user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
});
// in `src/routes/users/create.ts`
import { Action, io } from "@interval/sdk";
export default new Action({
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
});
// in `src/interval.ts`
import Interval, { Page, io } 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}`;
},
users: new Page({
name: "Users",
handler: async () => {
return new Layout({
title: "Beta users",
description: "These users have signed up for the beta.",
metadata: [
{
label: "Total users",
value: 2,
},
{
label: "Active users",
value: 1,
},
],
menuItems: [
{
label: "Create user",
action: "users/create",
},
{
label: "View metrics",
action: "hello_world",
},
],
children: [
io.display.table("Users", {
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bartholomew", email: "bart@example.com" },
],
}),
],
});
},
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
update: {
name: "Edit user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
}),
},
});
interval.listen();
info
Standalone object or function exports are not supported for file-based
actions; always use the Action
and Page
class wrappers.