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.
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.
Implement functions in three steps:
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
.
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:
env
using the CLI's var add
commandYou 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.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.
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.
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();
}
}