The new Slack platform and the features described below are in beta and under active development.

Datastores

Datastores make it easy to store and retrieve data in your Run On Slack apps.

You can store most supported or custom types.

Initialize a datastore

To initalize a datastore:

The information stored when initializing your datastore using slack run will be completely separate from the information stored in your datastore when using slack deploy.

Defining a datastore

To keep your app tidy, datastores can be defined in their own source files just like custom functions.

If you don't already have one, create a datastores directory in the root of your project, and inside, create a source file to define your datastore.

In the following example, we'll create a datastore called "Good Tunes" and define it in a file named good_tunes_datastore.ts. It will hold information about music artists and their songs:

// good_tunes_datastore.ts 
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";

const GoodTunesDatastore = DefineDatastore({
  name: "good_tunes",
  primary_key: "id",
  attributes: {
    id: {
      type: Schema.types.string,
    },
    artist: {
      type: Schema.types.string,
    },
    song: {
      type: Schema.types.string,
    },
  },
});

Datastores require three primary properties:

  • name: to identify your datastore
  • primary_key: the primary attribute to be used as the datastore's primary key (ensure this is an actual attribute that you have defined)
  • attributes: to scaffold your datastore's columns

Adding the datastore to your app's manifest

The last step in initializing your datastore is to add it to the datastores property in your manifest and include the required datastore bot scopes.

To do that, first add the datastores property to your manifest if it does not exist, then list the datastores you have defined. Second, add the following datastore permission scopes to the botScopes property:

  • datastore:read
  • datastore:write

Here's an example manifest definition given the above GoodTunesDatastore:

export default Manifest({
  name: "Good Tunes",
  description: "Add a song to the Good Tunes queue",
  icon: "assets/icon.png",
  functions: [ReverseFunction, GetCustomerInfoFunction],
  outgoingDomains: [],
  datastores: [GoodTunesDatastore],
  botScopes: [
    "commands", 
    "chat:write", 
    "chat:write.public", 
    "datastore:read", 
    "datastore:write"
  ],
});

Note that we've also added the required datastore:read and datastore:write bot scopes.

Interact with a datastore

You can interact with your app's datastore in your Run On Slack functions by importing and using SlackAPI.

Before creating or retrieving items, ensure that you have:

  • Imported SlackAPI from deno-slack-api (Step 1 here)
  • Added the token helper to your function (Step 2 here)
  • Instantiated a SlackAPI client (Step 3 here)

For the following examples, let's revisit the GoodTunesDatastore datastore we just initalized.

Visualizing the datastore

When interacting with your datastore, it may be helpful to first visualize its structure. In our GoodTunesDatastore example, let's say we have stored the following artists and their songs:

id artist song
1 Aretha Franklin Respect
2 The Beatles Yesterday
3 Led Zeppelin Stairway to Heaven
4 Whitney Houston I Will Always Love You
5 Fred Rogers Won't You be My Neighbor?

Creating or updating an item

Create or update an item by passing in values for each of the datastore's attributes:

// functions/insert_into_datastore.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";

export const InsertIntoDatastoreFunctionDefinition = DefineFunction({
  callback_id: "insert_into_datastore",
  title: "Insert into datastore",
  description: "Adds artist and song to a datastore",
  source_file: "functions/insert_into_datastore.ts",
  input_parameters: {
    properties: {
      artist: {
        type: Schema.types.string,
        description: "The name of the artist",
      },
      song: {
        type: Schema.types.string,
        description: "The name of the artist's song",
      },
    },
    required: ["artist", "song"],
  },
});

export default SlackFunction(
  InsertIntoDatastoreFunctionDefinition,
  async ({ inputs, client }) => { // Note the `async`, required since we `await` any `client` call.
    
    // The below will create a *new* item, since we're creating a new ID:
    const uuid = crypto.randomUUID();

    const putResponse = await client.apps.datastore.put({
      datastore: "good_tunes",
      item: {
        id: uuid, // To update an existing item, pass the `id` returned from a previous put command
        artist: inputs.artist,
        song: inputs.song,
      },
    });

    if (!putResponse.ok) {
      console.log("Error calling apps.datastore.put:");
      return await {
        error: putResponse.error,
        outputs: {}
      };
    } else {
      console.log("Datastore put successful!");
      return await {
        outputs: {},
      };
    }
  },
);


If the call was successful, the payload's ok property will be true, and the item property will return a copy of the data you just inserted:

{
  "ok":true,
  "datastore":"good_tunes",
  "item":{
    "artist":"The artist you entered",
    "song":"The song you entered",
    "id": "unique_id_for_this_item"
  }
}

If the call was not successful, the payload's ok property will be false, and you will have a error code and message property available:

{
  "ok":false,
  "error":"datastore_error",
  "errors":[
    {
      "code":"some_error_code",
      "message":"A description of the error",
      "pointer":"/datastore/good_tunes"
    }
  ]
}

Retrieving a single item

Now, let's retrieve an item by its primary key attribute. For example, if your primary key attribute is id, you could do the following:

// Somewhere in your function:

let uuid = "5";

const getResponse = await client.apps.datastore.get({
  datastore: "good_tunes",
  id: uuid
});

if (!getResponse.ok) {
  console.log(JSON.stringify(getResponse));
  return await {
    error: getResponse.error,
    outputs: {},
  };
}

If the call was successful and data was found, an item property in the payload will include the attributes (and their values) from the datastore definition:

{
  "ok":true,
  "datastore":"good_tunes",
  "item":{
    "artist":"Fred Rodgers",
    "song":"Won't You be My Neighbor?",
    "id":"5"
  }
}

If the call was successful but no data was found, the item property in the payload will be blank:

{
  "ok":true,
  "datastore":"good_tunes",
  "item":{}
}

If the call was unsuccessful, the payload will contain two fields:

{
  "ok":false,
  "error":"(some error string)"
}

Querying the datastore

If you need to retrieve more than a single row or find data without already knowing the id, you'll want to run a query. The apps.datastore.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.

Parameter Required? Description
datastore Required A string with the name of the datastore to read the data from
expression Optional A DynamoDB filter expression
expression_attributes 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
cursor Optional The string value to access the next page of results

Here's an example of how to query our GoodTunesDatastore and retrieve a list of all the songs with names starting with "Won't":

let result = await client.apps.datastore.query({
  datastore: "good_tunes",
  expression: "begins_with (#song_name, :name)",
  expression_attributes: { "#song_name": "song" },
  expression_values: { ":name": "Won't" },
});

In this example, we're doing the following:

  1. Defining a temporary variable #song_name to use when iterating through the :name (rows) of the song (column).
  2. If the :name value begins with "Won't", return that item.

Here's another example with GoodTunesDatastore where we retrieve all the songs written by Fred Rodgers:

let result = await client.apps.datastore.query({
  datastore: "good_tunes",
  expression: "#artist_name = :name",
  expression_attributes: { "#artist_name": "artist" },
  expression_values: { ":name": "Fred Rodgers" },
});

Similarly, in this example, we're doing the following:

  1. Defining a temporary variable #artist_name to use when iterating through the :name (rows) of the artist (column).
  2. If the :name value matches "Fred Rodgers", return that item.
Pagination

For cursor-paginated methods, use the cursor parameter to retrieve the next page of your query results.

If your initial query has another page of results, the next_cursor response parameter is the key returned that will unlock your next page of results. Use this key to query the datastore again and set cursor to the value of next_cursor.

Support for auto-pagination coming soon!

Express yourself

An expression is a way to build a query using comparison operators like =, <, <=.

An expression_attributes object is a map of the columns used for the comparison, and an expression_values object is a map of values. The expression_attributes property must always begin with a #, and the expression_values property 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 datastore), you will receive a cryptic error. Please use apps.datastore.get instead. We're hard at work on making these types of errors easier to understand!

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_attributes: { "#assignee": "assignee"},
	expression_values: { ":user": inputs.user }

A full list of comparison operators is below:

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")

Generic types for datastores

You can provide your datastore's definition as a generic type, which will provide some automatic typing on the arguments and response:

// If you have a datastore definition...
const MyDatastore = {
  name: "my_datastore",
  attributes: {
    id: "string",
    email: "string"
  },
  primary_key: "id",
}

// ... you can use it as the generic type reference like this...
await client.apps.datastore.put<typeof MyDatastore.definition>({
  datastore: "my_datastore",
  item: {
    id: `${Date.now()}`,
    email: "test@test.com"
  },
});

You can use the result of your DefineDatastore() call as the type by using its definition property:

const MyDatastore = DefineDatastore({
  name: "my_datastore",
  attributes: {
    id: "string",
    email: "sring"
  },
  primary_key: "id",
});

await client.apps.datastore.put<typeof MyDatastore.definition>({...});

By using typed methods, the datastores property (e.g. MyDatastore.datastore) will enforce its value matches the datastore definition's name property across methods and the item matches the definition's attributes in arguments and responses. Also, for get() and delete(), a property matching the primary_key will be expected as an argument.

Troubleshooting

If you're getting errors, check the following:

  • The primary key is formatted as a string
  • The datastore is included in the manifest's datastores property
  • The datastore bot scopes are included in the manifest (datastore:read and datastore:write)

Have 2 minutes to provide some feedback?

We'd love to hear about your experience with the new Slack platform. Please complete our short survey so we can use your feedback to improve.