Skip to content

JsWorkflows provides a platform-managed Slack OAuth app. You do not need to create your own Slack app or manage client credentials.

ResourceOperationScope
MessagingSend messagechat:write, users:read
MessagingRead messageschannels:history, groups:history, im:history, mpim:history
UsersGet user info / list usersusers:read
ChannelsList channelschannels:read
ChannelsJoin channelchannels:join
FilesUpload filefiles:write
FilesRead filesfiles:read
ReactionsAdd/read reactionsreactions:write, reactions:read
export class Workflow {
async start(data, _headers, api) {
const { token, error } = await api.getOAuthToken('my-slack');
if (error || !token) throw new Error(error || 'Missing Slack token');
const response = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
channel: 'C0123456789',
text: `New order #${data.order_number}: ${data.total_price} ${data.currency}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*New order* #${data.order_number}: ${data.total_price} ${data.currency}`,
},
},
],
}),
});
if (!response.ok) {
throw new Error(`Slack HTTP ${response.status}: ${await response.text()}`);
}
const result = await response.json();
if (!result.ok) {
throw new Error(`Slack API error: ${result.error || 'unknown_error'}`);
}
}
}

Use a channel ID when possible. It is the safest value to store in workflow config and pass to Slack APIs.

Slack’s current upload flow is:

  1. call files.getUploadURLExternal
  2. upload the file bytes to the returned upload_url
  3. call files.completeUploadExternal
const { token, error } = await api.getOAuthToken('my-slack');
if (error || !token) throw new Error(error || 'Missing Slack token');
const csvString = 'sku,available\\nABC-123,42\\n';
const fileBytes = new TextEncoder().encode(csvString);
const initResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
filename: 'report.csv',
length: fileBytes.byteLength,
}),
});
if (!initResponse.ok) {
throw new Error(`Slack HTTP ${initResponse.status}: ${await initResponse.text()}`);
}
const initResult = await initResponse.json();
if (!initResult.ok) {
throw new Error(`Slack API error: ${initResult.error || 'unknown_error'}`);
}
const uploadResponse = await fetch(initResult.upload_url, {
method: 'POST',
headers: { 'Content-Type': 'text/csv; charset=utf-8' },
body: fileBytes,
});
if (!uploadResponse.ok) {
throw new Error(`Slack upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`);
}
const completeResponse = await fetch('https://slack.com/api/files.completeUploadExternal', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
files: [{ id: initResult.file_id, title: 'report.csv' }],
channel_id: 'C0123456789',
initial_comment: 'Daily inventory report',
}),
});
if (!completeResponse.ok) {
throw new Error(`Slack HTTP ${completeResponse.status}: ${await completeResponse.text()}`);
}
const completeResult = await completeResponse.json();
if (!completeResult.ok) {
throw new Error(`Slack API error: ${completeResult.error || 'unknown_error'}`);
}

For file sharing, use channel_id or channels with Slack IDs. Do not use #channel-name in the file upload completion request.