Our future generation platform is in closed beta. Please request to participate. During the beta, expect some rough edges, broken windows overlooking blue sky vistas, and regularly scheduled changes.

Storing data with tables

If your app needs to store data, use the Tables API. Like functions and workflows, tables are defined via files located in the tables directory at the root. There are also new API methods for writing, reading, querying, and deleting data.

Apps can use tables for varying complexity of data requirements. The Tables API can support simple implementations, like maintaining state or associating account IDs across multiple services, up to complex, custom data structures.

Developing locally with tables

Currently, if you are developing locally, any function calls that include a Tables API method call will work and the tables that are generated will be associated with the local, non-hosted app. This is will likely change as the beta period ends and is meant to allow for local development and testing only.

OAuth scopes

The Slack API uses OAuth to grant access to API methods and requires all access tokens to be granted specific scopes in order to access the corresponding API methods. In order to use the Tables API, an access token must request and be granted the tables:read OAuth scope in order to read data from a table and the tables:write scope in order to write data.

Most applications using tables will likely request both but it's not a requirement.

Defining a table

Before any data can be stored in a table, the data structure needs to be defined in a file in the tables directory. An app can have multiple tables and each table has column types that match the types for functions and workflows:

Type Shorthand Full SDK reference Description
String string Schema.types.string UTF-8 encoded string, up to 4000 bytes
Boolean boolean Schema.types.boolean a logical value, must be either true or false
Integer integer Schema.types.integer a whole number, such as -1, 0 or 31415926535
Slack User ID user_id Schema.slack.types.user_id a Slack user ID such as U18675309 or W15556162
Slack Usergroup ID usergroup_id Schema.slack.types.channel_id a Slack user ID such as S0614TZR7
Slack Channel ID channel_id Schema.slack.types.channel_id a Slack channel ID such as C123ABC45 or D987XYZ65

An example table schema file:

import { DefineTable, Schema } from "slack-cloud-sdk/mod.ts";

export const Reversals = DefineTable("reversals", {
  primary_key: "id",
  columns: {
    id: {
      type: Schema.types.string,
    },
    original_string: {
      type: Schema.types.string,
    },
    reversed_string: {
      type: Schema.types.string,
    },
    user_id: {
      type: Schema.slack.types.user_id,
    },
  },
});

After you're happy with your table definition, run slack deploy to create the table for your app.

Altering table schemas
When altering a table schema, tread carefully! Changing the name of a table currently results in two tables (the old table and its data will continue to exist.) Adding new columns will result in missing data for rows that existed before the column was added.

Table validation is still a work in progress. Currently, the following validations will be applied:

  • Check for valid column types
  • Detect duplicate table names
  • Detect duplicate column names
  • Find empty column names and types
  • Ensure a column with type 'id' is present
  • Check for deleted/renamed columns
  • Check for deleted/renamed tables
  • Check for data type change in an existing column

Create, read, update, and delete data

Once your table is defined and deployed, use API methods to add new rows, read, and delete data. The methods will typically be called from within a function. These can be called using the built-in client included with the SDK like so:

client.call("apps.hosted.tables.query", { ... });

Shorthand for calling tables methods

In addition to calling the API methods with their full method name, the SDK includes a bit of syntactic sugar to make working with tables a bit easier. The tables object you create using DefineTables includes an api method that accepts a client, which allows you to create a new object for calling the API methods.

const tables = Reversals.api(client);

tables.put({
	row: {
		id: 1,
		original_string: "Hello, world!",
		reversed_string: "!dlrow ,olleH",
		user_id: "W0123456789"
	}
})

Create a new row

Add data to a table by creating new rows using the apps.hosted.tables.putRow API method. This method requires two parameters.

apps.hosted.tables.putRow

Parameter Required? Description
table Required A string with the name of the table to insert the data into
row Required An object with column name and value pairs. The structure of this object should match the table definition. All of the columns in the table definition are always optional, but you can't supply data for any columns that aren't defined.
await client.call("apps.hosted.tables.putRow", {
	table: "reversals",
	row: {
		id: 1,
		original_string: "Hello, world!",
		reversed_string: "!dlrow ,olleH",
		user_id: "W0123456789"
	}
  });

Shorthand:

const tables = TasksTables.api(client);

tables.put({
	id: 1,
	original_string: "Hello, world!",
	reversed_string: "!dlrow ,olleH",
	user_id: "W0123456789"
});

Read data from a table

The easiest way to read data from a table is to retrieve a specific row via its id using the apps.hosted.tables.getRow API method. This method requires two parameters.

apps.hosted.tables.getRow

Parameter Required? Description
table Required A string with the name of the table to read the data from
id Required A string matching the id of the row you want to retrieve.

This will respond with an object containing the column and value pairs for the row.

await client.call("apps.hosted.tables.getRow", {
    table: "reversals",
    id: "1"
  });

Shorthand:

const tables = Reversals.api(client);

tables.get(1);

apps.hosted.tables.query

Of course, if you need to retrieve more than a single row or find data without already knowing the id, you'll want to query a table. The apps.hosted.tables.query API method uses DynamoDB syntax to do just that. If you are used to other database query syntaxes, DynamoDB's filtering is a bit different though very powerful.

Here's an example of how to query the todos table and retrieve a list of all todos created today:

let result = await client.call("apps.hosted.tables.query", {
	table: "reversals",
		expression: "#created = :today",
		expression_columns: { "#created": "created"},
		expression_values: { ":today": moment().startOf('day').utc().format()}
});

Shorthand:

const tables = Reversals.api(client);

tables.query({
	expression: "#created = :today",
	expression_columns: { "#created": "created"},
	expression_values: { ":today": moment().startOf('day').utc().format()}
});
Parameter Required? Description
table Required A string with the name of the table to read the data from
expression Optional A DynamoDB filter expression (see below)
expression_columns Optional A map of columns used by the expression
expression_values Optional A map of values used by the expression
limit Optional The maximum number of entries to return, 1-1000 (both inclusive); default is 100
Express yourself

An expression is a way to build a query using comparison operators like =, <, <=. An expression_columns object is a map of the columns used for the comparison, and an expression_values object is a map of values. The expression_column must always begin with a # and the expression_values must always begin with a :

Expressions can only contain non-primary key attributes
If you try to write an expression that uses a primary key as its attribute (for example, to pull a single row from a database), you will receive a cryptic error. Please use apps.hosted.tables.getRow instead. We're hard at work on making these types of errors easier to understand!

You will often want to calculate the values of expression_values. For example, to get the current date (using moment.js), you could use:

{":today": moment().startOf('day').utc().format()}

Here's an example from a function that receives a user ID via an input and queries for every todo item assigned to that person:

expression: "#assignee = :user",
	expression_columns: { "#assignee": "assignee"},
	expression_values: { ":user": inputs.user }

A full list of comparison operators:

Operator Description Example
= True if both values are equal a = b
< True if the left value is less than but not equal to the right a < b
<= True if the left value is less than or equal do the right a <= b
> True if the left value is greater than but not equal to the right a > b
>= True if the left value is greater than or equal do the right a >= b
BETWEEN ... AND True if one value is greater than or equal to one and less than or equal to another a BETWEEN b AND c
(a: 6,b: 3, c: 6)
begins_with(str, substr) True if a string begins with substring begins_with("racecar", "race")

Update data in a table

To update a row, use the same apps.hosted.tables.putRow method method you used to create a new row with the id of the existing row and new column/value pairs. For completeness sake, include all values for the columns in a table.

Delete data in a table

apps.hosted.tables.deleteRow

To delete a row, use the apps.hosted.tables.deleteRow API method, passing it the table name and id of the row.

Parameter Required? Description
table Required A string with the name of the table to read the data from
id Required A string with the id of the row

Shorthand:

tables = Reversals.api(client);

tables.delete(31415);

Onward

Now that you've created a database, you can write a function to complete a task or deploy your app to Slack infrastructure.

Was this page helpful?