In order to develop on the next-generation platform, your workspace must be on a paid plan.
With next generation Slack apps, you can build custom functions, which are reusable building blocks of automation deployed to our infrastructure. They accept inputs, perform calculations, and provide outputs, just like typical programmatic functions. Custom functions can be used as steps in workflows—and workflows are invoked by triggers.
When building workflows using functions, note that there is a 15 second timeout for a deployed function and a 3 second timeout for a locally-run function. If the function has not finished running within its respective time limit, you will see an error in your log.
Functions are defined via the DefineFunction
method, which is part of the Slack SDK that is included with every newly-created project. Both the definition and implementation for your functions should live in the same file, so to keep your app organized, put all your function files in a functions
folder in your app's root folder.
Let's take a look at the greeting_function.ts
within the Hello World sample app:
// /slack-samples/deno-hello-world/functions/greeting_function.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
export const GreetingFunctionDefinition = DefineFunction({
callback_id: "greeting_function",
title: "Generate a greeting",
description: "Generate a greeting",
source_file: "functions/greeting_function.ts",
input_parameters: {
properties: {
recipient: {
type: Schema.slack.types.user_id,
description: "Greeting recipient",
},
message: {
type: Schema.types.string,
description: "Message to the recipient",
},
},
required: ["message"],
},
output_parameters: {
properties: {
greeting: {
type: Schema.types.string,
description: "Greeting for the recipient",
},
},
required: ["greeting"],
},
});
Note that we import DefineFunction
, which is used for defining our function, and also SlackFunction
, which we'll use to implement our function in the Implement a custom function section.
Field | Description | Required? |
---|---|---|
callback_id |
A unique string identifier representing the function. No other functions in your application may share a callback ID. Changing a function's callback ID is not recommended, as the function will be removed from the app and created under the new callback ID, breaking any workflows referencing the old function. | Yes |
title |
A string to nicely identify the function. | Yes |
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. | Yes |
description |
A succinct summary of what your function does. | No |
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 available to your function. | No |
output_parameters |
An object which describes one or more output parameters that will be returned by your function. Each top-level property of this object defines the name of one output parameter your function makes available. | No |
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.
A custom function's input_parameters
and output_parameters
properties have two sub-properties:
required
, which is how you can ensure that a function requires a specific parameter.properties
, where you can list the specific parameters that your function accounts for.Parameters are listed in the properties
sub-property. The value for a parameter needs to be an object with further sub-properties:
type
: The type of the input parameter. This can be a built-in type or a custom type that you define.description
: A string description of the parameter.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"]
}
If your input or output parameter is a custom type with required sub-properties, use the DefineProperty
function to to ensure that each sub-property's required status is respected. Let's look at an example. Given an input_parameter
of msg_context
with three sub-properties, message_ts
, channel_id
, and user_id
, this is how we would ensure that message_ts
is required:
const messageAlertFunction = DefineFunction({
...
input_parameters: {
properties: {
msg_context: DefineProperty({
type: Schema.types.object,
properties: {
message_ts: { type: Schema.types.string },
channel_id: { type: Schema.types.string },
user_id: { type: Schema.types.string },
},
required: ["message_ts"]
})
}
},
});
Check out Typescript-friendly type definitions for more details.
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.
After defining your custom function, declare it in your app's manifest file:
// /manifest.ts
// Import the function
import { GreetingFunctionDefinition } from "./functions/greeting_function.ts"
// ...
export default Manifest({
//...
functions: [GreetingFunctionDefinition],
//...
});
Once your function is defined in your app's manifest file, the next step is to implement the function in its respective source file.
To keep your project tidy, implement your functions in the same source file in which you defined them.
Implementation involves creating a SlackFunction
default export. This example is again from the greeting_function.ts
within the Hello World sample app:
// /slack-samples/deno-hello-world/functions/greeting_function.ts
}); // end of DefineFunction
export default SlackFunction(
// Pass along the function definition from earlier in the source file
GreetingFunctionDefinition,
({ inputs }) => { // Provide any context properties, like `inputs`, `env`, or `token`
// Implement your function
const { recipient, message } = inputs;
const salutations = ["Hello", "Hi", "Howdy", "Hola", "Salut"];
const salutation =
salutations[Math.floor(Math.random() * salutations.length)];
const greeting =
`${salutation}, <@${recipient}>! :wave: Someone sent the following greeting: \n\n>${message}`;
// Don't forget any required output parameters
return { outputs: { greeting } };
},
);
It is important to store your environment variables, as custom functions deployed to Slack will not run with the --allow-env
permission. When locally running your app using slack run
, the CLI will automatically load your local .env
file and populate the env
function input parameter. However, when deploying your app using slack deploy
, the values you added using slack env add
will be available in the env
function input parameter. Refer to environment variables for more information.
Similarly, when using a locally running your app, you can use console.log
to emit information to the console. However, when your app is deployed to production, any console.log
commands are available via slack activity
. Check out our Logging page for more.
When building workflows using functions, note that there is a 15 second timeout per function. If the function has not finished running within 15 seconds from the time it started, you will see a timeout error in your log.
When composing your functions, you can:
slack env add
commandYou can also encapsulate your business logic separately from the function handler, then import what you need and build your functions that way.
Your function handler's context supports several properties that you can use by declaring them.
Here are all the context properties available:
Property | Kind | Description |
---|---|---|
env |
String | Represents environment variables available to your function's execution context. A locally running app gets its env properties populated via the local .env file. A deployed app gets its env properties populated via the CLI's slack env add command. |
inputs |
Object | Contains the input parameters you defined as part of your function definition. |
client |
Object | An API client ready for use in your function. Useful for calling Slack API methods. |
token |
String | Your application's access token. |
event |
Object | Contains the full incoming event details. |
team_id |
String | The ID of your Slack workspace, i.e. T123ABC456. |
enterprise_id |
String | The ID of the owning enterprise organization, i.e. "E123ABC456". Only applicable for Slack Enterprise Grid customers, otherwise its value will be set to an empty string. |
The object returned by your function supports the following properties:
Property | Kind | Description |
---|---|---|
error |
String | Indicates the error that was encountered. If present, the function will return an error regardless of what is passed to outputs. |
outputs |
Object | Exactly matches the structure of your function definition's output_parameters. This is required unless an error is returned. |
completed |
Boolean | Indicates whether or not the function is completed. This defaults to true . |
➡️ To keep building your app, head to the workflows section to learn how to add a custom function to a workflow.
⤵️ To learn how to distribute your custom function, read on!
A newly-created custom function will only be accessible to its creator until it is distributed to others.
To distribute a function so that another user (or many users) can build workflows that reference that function, you'll use the distribute
command. At this time, functions can be distributed to:
In order to enable the distribute
command, your app must have been deployed at least once before attempting to distribute your function to others.
Re-deploy your app after using the distribute function
Anytime you make permission changes to your function using the distribute
command, your app must be redeployed, each time after, in order for the new access changes to be available in your app's workspace.
Given:
get_next_song
U1234567
You can distribute the function get_next_song
to the user U1234567
like this:
$ slack function distribute --name get_next_song --users U1234567 --grant
To revoke access, replace --grant
with --revoke
.
Given:
calculate_royalties
U1111111
, U2222222
, and U3333333
You can distribute the function calculate_royalties
to the above users like this:
$ slack function distribute --name calculate_royalties --users U1111111,U2222222,U3333333 --grant
To revoke access, replace --grant
with --revoke
.
Given:
notify_escal_team
You can distribute the function notify_escal_team
to all of your app's collaborators like this:
$ slack function distribute --name notify_escal_team --app_collaborators --grant
Given:
get_customer_profile
You can distribute the function get_customer_profile
to everyone in your workspace like this:
$ slack function distribute --name get_customer_profile --everyone --grant
The prompt-based approach allows you to distribute your function to one user, to multiple people, to collaborators, or to everyone in an interactive prompt.
To activate the flow, use the following command in your terminal:
$ slack function distribute
Given:
reverse
You will answer the first prompt in the following manner:
> reverse (Reverse)
If going from everyone
or app_collaborators
to specific users, you should be offered the option of adding collaborators to specific users.
> specific users (current)
app collaborators only
everyone
> granting a user access
revoking a user's access
Given:
U0123456789
You will answer the following prompt below:
: U0123456789
You can add multiple users at the same time. To do this, separate the user IDs with a comma (e.g. U0123456789
, UA987654321
).
After you've finished this flow, you'll receive a message indicating the type of distribution you chose.
For more distributions options, including how to revoke access, head to the distribute command reference.
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 an error
property:
import { SlackFunction } from "deno-slack-sdk/mod.ts";
import type { GetCustomerNameFunction } from "../manifest.ts";
import { GetCustomerInfo } from "../mycorp/get_customer_info.ts";
export default SlackFunction(
GetCustomerNameFunction,
async ({inputs, client}) => {
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 {
error: `Could not find customer where ID == ${inputs.customer_id}!`,
outputs: {},
};
}
}
return {
outputs: {
first_name: response?.first_name,
last_name: response?.last_name,
},
};
});
// 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",
};
// Maybe here there's some third-party API we call
return customer;
} else {
throw new Deno.errors.NotFound();
}
}
Have 2 minutes to provide some feedback?
We'd love to hear about your experience building modular Slack apps. Please complete our short survey so we can use your feedback to improve.