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

Already signed up? Great! You can ignore this.
Need to sign up? Request to participate.

Creating functions

On the next-generation platform, you can build Run On Slack functions, reusable building blocks of automation that are deployed to our infrastructure and accept inputs, perform some calculations, and provide outputs. Functions can be triggered via Global Shortcut, and we'll be adding support for more function and trigger types in the coming months.

To create a Run On Slack function, we need to do two things: define the function in the Manifest, and implement the function in its respective source file.

Defining a function

Functions are defined in your app project's manifest file (manifest.ts) via the DefineFunction method, which is part of the SDK that gets included with every newly created project.

Let's take a look at an empty function:

const NothingFunction = DefineFunction({
  callback_id: "nothing",
  title: "Do Nothing",
  description: "",
  source_file: "functions/nothing.ts",
  input_parameters: {
    properties: {},
    required: [],
  },
  output_parameters: {
    properties: {},
    required: [],
  }
});

A few things stand out, like how there are sections for explicitly setting the input and output parameters. We also configure the title, description, and point to the source file where the function is implemented.

Function definitions require the following fields:

Field Description
callback_id A unique string identifier representing the function ("nothing" in the above example). It must be unique in your application; no other functions may share the same callback ID. Changing a function's callback ID is not recommended as it means that the function will be removed from the app and created under the new callback ID, which will break any workflows referencing the old function.
title A pretty string to nicely identify the function.
description A succinct summary of what your function does.
source_file The relative path from the project root to the function handler file (i.e., the source file). Remember to update this if you start nesting your functions in folders.
input_parameters An object which describes one or more input parameters that will be available to your function. Each top-level property of this object defines the name of one input parameter which will become available to your function.
output_parameters An object which describes one or more output parameters that will be returned by your function. This object follows the exact same pattern as input_parameters: top-level properties of the object define output parameter names, with the property values further describe the type and description of each individual output parameter.

The value for properties in input_parameters and output_parameters needs to be an object with further sub-properties:

  • type: The type of the input parameter. The supported types are string, boolean, object, and array. Support for more types coming soon.
  • description: A string description of the input parameter.

If you want to set a property as required, list its name in its respective input or output properties required property.

For example, if you have an input parameter named customer_id that you want to be required, you can do so like this:

input_parameters: {
  properties: {
    customer_id: {
      type: Schema.types.string,
      description: "The customer's ID"
    }
  },
  required: ["customer_id"]
}

Below is an annotated example of the reverse string function named ReverseFunction that comes with every newly created Run On Slack app:

// Declare a const var to hold your function's definition
const ReverseFunction = DefineFunction({
  // An internal, unique identifier for this function
  callback_id: "reverse",

  // How your users will see the function (e.g., in the shortcuts menu)
  title: "Reverse",

  // Helpful explainer for your users about what the function does
  description: "Takes a string and reverses it",

  // The source file where this function is implemented
  source_file: "functions/reverse.ts",

  input_parameters: {
    
    properties: {
      // An input variable named `stringToReverse` of type `string`
      stringToReverse: {
        type: Schema.types.string,
        description: "The string to reverse",
      },
    },

    // Any input parameters that are required
    required: ["stringToReverse"],
  },

  output_parameters: {
    
    properties: {

      // An output variable named `reverseString` of type `string`
      reverseString: {
        type: Schema.types.string,
        description: "The string in reverse",
      },
    },

    // Any output parameters that are required
    required: ["reverseString"],
  },
});

Functions can (and generally should) declare inputs and outputs.

Inputs are declared in the input_parameters property, and outputs are declared in the output_parameters property. Both must be one of the following types:

Type Kind Description
Schema.types.string String UTF-8 encoded string, up to 4000 bytes
Schema.types.boolean Boolean a logical value, must be either true or false
Schema.slack.types.user_id Slack User ID a Slack user ID such as U18675309 or W15556162
Schema.slack.types.channel_id Slack Channel ID a Slack channel ID such as C123ABC45 or D987XYZ65

While, strictly speaking, input and output parameters are optional, they are a common and standard way to pass data between functions and nearly any function you write will expect at least one input and pass along an output.

Functions are similar in philosophy to Unix system commands: they should be minimalist, modular, and reusable. Expect the output of one function to eventually become the input of another, with no other frame of reference.

Once your function is defined in your app's manifest file, the next step is to implement the function in its respective source file.

Implementing a function

Implement functions in three steps:

  1. Create the source file
  2. Write code in the source file
  3. Declare the function in the Manifest

Step 1: Create the source file

Functions are implemented in their respective source files—and their source files don't exist until you create them.

To create a source file for your function, create a new file in the functions folder that corresponds to your function's callback_id. For example, if your function's callback_id is get_customer_name, your file must be named get_customer_name.ts.

Step 2: Write code in the source file

The default export of your function's source file will be either a single function handler. It can be either asynchronous (for example, if you're calling API methods) or sychronous.

The function takes a single argument called its "context", and returns an object that exactly matches the structure of function definition's output_parameters.

Let's look at the function that comes with every newly created project, a function that reverses a string given by a user:

// functions/reverse.ts
import type { FunctionHandler } from "deno-slack-sdk/types.ts";

// deno-lint-ignore no-explicit-any
const reverse: FunctionHandler<any, any> = async ({ inputs, env }) => {

  // The input parameter `stringToReverse` is accessed via `inputs`.
  console.log(`reversing ${inputs.stringToReverse}.`);
  
  // Environment variables are accessed via `env`.
  console.log(`SLACK_API_URL=${env["SLACK_API_URL"]}`);

  const reverseString = inputs.stringToReverse.split("").reverse().join("");

  return await {
    // Any required outputs *must* be included here
    outputs: { reverseString },
  };
};

export default reverse;

Note that return await is used because the function handler is instantiated as async; you can remove both async and await from the above example in cases where you do not need asynchronous functionality.

When using a local development server, you can use console.log to emit information to the console. When your app is deployed to production, any console.log commands are ignored.

When composing your functions, some things you can do include:

You can also encapsulate your business logic separately from the Run On Slack function handler, then import what you need and build your functions that way.

Let's look at an example of a more complex function called Get Customer Name.

Here's its definition:

// in manifest.ts
const GetCustomerNameFunction = DefineFunction({
  callback_id: "get_customer_name",
  title: "Get Customer Name",
  description: "Returns a customer's first and last name",
  source_file: "functions/get_customer_name.ts",
  input_parameters: {
    required: ["customer_id"],
    properties: {
      customer_id: {
        type: Schema.types.string,
        description: "The customer's ID",
      },
    },
  },
  output_parameters: {
    required: ["first_name", "last_name"],
    properties: {
      first_name: {
        type: Schema.types.string,
        description: "Customer's first name",
      },
      last_name: {
        type: Schema.types.string,
        description: "Customer's last name",
      },
    },
  },
});

As defined, the function takes a customer ID as input, then returns that customer's first and last name.

To see how it does that, let's look at the function definition:

// functions/get_customer_name.ts

import type { FunctionHandler } from "deno-slack-sdk/types.ts";

// Here we import custom business logic
import { MyCorpGetCustomerName } from "../mycorp/get_customer_name.ts";

// Declare a new function handler
const get_customer_name: FunctionHandler<any, any> = async ({ inputs, env }) => {
  
  console.log(`Getting first and last name for customer ID ${inputs.customer_id}...`);

  // Call out to our in-house services to retrieve a customer's name
  let response = MyCorpGetCustomerName(inputs.customer_id);
  
  // Return the fields in `outputs`
  return await {
    outputs: {
      // output_parameters on left, values to return on right
      first_name: response.first_name,
      last_name: response.last_name,
    },
  };
};

// Make our function handler the default export
export default get_customer_name;

The above function get_customer_name takes a customer ID as an input parameter, passes that ID to the MyCorpGetCustomerName function to retrieve a customer's first and last name, then returns the retrieved name strings associated with the given ID.

Your function handler's context supports several helper objects that you can use by declaring them. In the get_customer_name example, two helpers are used: inputs and env.

Here are all the helpers available to you:

  • env: Represents environment variables available to your function's execution context. These are added via the CLI's var add command.
  • inputs: An object containing the input parameters you defined as part of your Function Definition. In the example above, the name input parameter is available on the inputs property of our function handler context.
  • token: Your application's access token. Useful for calling Slack API methods.
  • event: An object containing the full incoming event details.

Step 3: Declare the function in the manifest

Your app will only know about your function when it is declared in the Manifest. Note that a function's definition is separate from its declaration in the Manifest's functions property.

To declare a function in the Manifest, locate the functions property and add the function's name to it.

For example, if I wanted to add a function named GetCustomerName to the default Manifest that comes with the reverse string app project, my Manifest might look something like this:

export default Manifest({
  name: "My App",
  description: "Reverse a string",
  runtime: "deno1.x",
  icon: "assets/icon.png",
  // Defined and implemented functions that I want to use should be listed here:
  functions: [ReverseFunction,GetCustomerName],
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

📖 Explore some examples of what you can do with functions in our samples repo. TODO

✨ When you're finished defining and implementing your functions, you're ready to start a local development server to begin interacting with your functions immediately.

Using environment variables

You can use environment variables that are accessible via the env helper in your Run On Slack functions.

When developing locally, environment variables present in a .env file at the root of your project will be made available via the env helper in your Run On Slack functions.

For example, given the following .env file:

MYCORP_API_TOKEN=asdf1234

We can retrieve the MYCORP_API_TOKEN environment variable in our Run On Slack functions like this:

import type { FunctionHandler } from "deno-slack-sdk/types.ts";

const get_some_info: FunctionHandler<any, any> = async ({ inputs, env }) => {
  
  const token = env['MYCORP_API_TOKEN']; // Retrieve the environment variable
  
  // ...

When you are ready to deploy your application to production, use var add to set environment variables; the .env will not be used when you run slack deploy. Environment variables added via the var command are also made available via the env helper in Run On Slack functions.

Graceful errors

You can use try-catch blocks in your Run On Slack functions to gracefully handle errors.

At this time, if you execute some logic that results in an error without catching that error, the Global shortcut modal will spin until it times out. To ensure that errors in your function are handled gracefully, consider wrapping your logic in a try-catch block, and ensure you're returning an empty outputs property along with error:

// functions/get_customer_info.ts

import type { FunctionHandler } from "deno-slack-sdk/types.ts";
import GetCustomerInfo from "../mycorp/get_customer_info.ts";

const get_customer_info: FunctionHandler<any, any> = async (
  { inputs, env },
) => {
  console.log(`Getting profile for customer ID ${inputs.customer_id}...`);
  let response;

  try {
    response = await GetCustomerInfo(inputs.customer_id);
  } catch (error) {
    if (error instanceof Deno.errors.NotFound) {
      return await {
        error: `Could not find customer where ID == ${inputs.customer_id}!`,
        outputs: {},
      };
    }
  }

  return await {
    outputs: {
      first_name: response?.first_name,
      last_name: response?.last_name,
    },
  };
};

export default get_customer_info;
// mycorp/get_customer_info.ts
export interface Customer {
  id: number;
  first_name: string;
  last_name: string;
}

export default function GetCustomerInfo(id: number): Customer {
  if (id == 1) {
    const customer: Customer = {
      id: 1,
      first_name: "Some",
      last_name: "Person",
    };
    return customer;
  } else {
    throw new Deno.errors.NotFound();
  }
}