# Slack Client

Send messages, manage channels, and interact with Slack workspaces.

## Methods

| Method                                   | Description                                       |
| ---------------------------------------- | ------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any Slack API request with schema validation |

## Usage

### Post a Message

```typescript
import { api, z, slack } from "@superblocksteam/sdk-api";

const PostMessageResponseSchema = z.object({
  channel: z.string(),
  ts: z.string(), // Message timestamp (unique ID)
  message: z.object({
    type: z.string(),
    text: z.string(),
    user: z.string().optional(),
    ts: z.string(),
  }),
});

// Integration ID from the integrations panel
const OPS_SLACK = "a1b2c3d4-5678-90ab-cdef-222222222222";

export default api({
  name: "SlackExample",
  integrations: {
    slack: slack(OPS_SLACK),
  },
  input: z.object({
    channel: z.string(),
    message: z.string(),
  }),
  output: z.object({
    messageId: z.string(),
  }),
  async run(ctx, { channel, message }) {
    const result = await ctx.integrations.slack.apiRequest(
      {
        method: "POST",
        path: "/chat.postMessage",
        body: {
          channel,
          text: message,
        },
      },
      { response: PostMessageResponseSchema },
    );

    if (!result.ok) {
      throw new Error(`Slack API error: ${result.error}`);
    }

    return { messageId: result.ts };
  },
});
```

### Post with Block Kit

```typescript
const result = await ctx.integrations.slack.apiRequest(
  {
    method: "POST",
    path: "/chat.postMessage",
    body: {
      channel: "#alerts",
      text: "New deployment", // Fallback text
      blocks: [
        {
          type: "header",
          text: {
            type: "plain_text",
            text: "Deployment Complete",
          },
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: "*Environment:* Production\n*Version:* v2.1.0",
          },
        },
        {
          type: "actions",
          elements: [
            {
              type: "button",
              text: { type: "plain_text", text: "View Logs" },
              url: "https://logs.example.com",
            },
          ],
        },
      ],
    },
  },
  { response: PostMessageResponseSchema },
);
```

### List Channels

```typescript
const ListChannelsResponseSchema = z.object({
  channels: z.array(
    z.object({
      id: z.string(),
      name: z.string(),
      is_private: z.boolean(),
      is_archived: z.boolean(),
      num_members: z.number().optional(),
    }),
  ),
  response_metadata: z
    .object({
      next_cursor: z.string().optional(),
    })
    .optional(),
});

const result = await ctx.integrations.slack.apiRequest(
  {
    method: "GET",
    path: "/conversations.list",
    params: {
      types: "public_channel,private_channel",
      limit: 100,
    },
  },
  { response: ListChannelsResponseSchema },
);

if (!result.ok) {
  throw new Error(`Slack API error: ${result.error}`);
}

result.channels.forEach((channel) => {
  console.log(`#${channel.name} (${channel.id})`);
});
```

### Get User Info

```typescript
const UserInfoResponseSchema = z.object({
  user: z.object({
    id: z.string(),
    name: z.string(),
    real_name: z.string(),
    profile: z.object({
      email: z.string().optional(),
      display_name: z.string(),
      image_72: z.string().optional(),
    }),
    is_admin: z.boolean(),
  }),
});

const result = await ctx.integrations.slack.apiRequest(
  {
    method: "GET",
    path: "/users.info",
    params: { user: "U123456789" },
  },
  { response: UserInfoResponseSchema },
);

if (!result.ok) {
  throw new Error(`Slack API error: ${result.error}`);
}

console.log(`User: ${result.user.real_name} (${result.user.profile.email})`);
```

### Upload a File

```typescript
const FileUploadResponseSchema = z.object({
  file: z.object({
    id: z.string(),
    name: z.string(),
    mimetype: z.string(),
    permalink: z.string(),
  }),
});

const result = await ctx.integrations.slack.apiRequest(
  {
    method: "POST",
    path: "/files.upload",
    body: {
      channels: "C123456789",
      filename: "report.csv",
      content: "name,value\nfoo,123\nbar,456",
      title: "Daily Report",
    },
  },
  { response: FileUploadResponseSchema },
);

if (!result.ok) {
  throw new Error(`Slack API error: ${result.error}`);
}

console.log(`File uploaded: ${result.file.permalink}`);
```

### Reply to a Thread

```typescript
const result = await ctx.integrations.slack.apiRequest(
  {
    method: "POST",
    path: "/chat.postMessage",
    body: {
      channel: "#alerts",
      text: "Thread reply message",
      thread_ts: "1234567890.123456", // Parent message timestamp
    },
  },
  { response: PostMessageResponseSchema },
);
```

### Update a Message

```typescript
const UpdateMessageResponseSchema = z.object({
  channel: z.string(),
  ts: z.string(),
  text: z.string(),
});

const result = await ctx.integrations.slack.apiRequest(
  {
    method: "POST",
    path: "/chat.update",
    body: {
      channel: "C123456789",
      ts: "1234567890.123456", // Message to update
      text: "Updated message text",
    },
  },
  { response: UpdateMessageResponseSchema },
);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await slack.postMessage({ ... });
await slack.listChannels();

// CORRECT - Use apiRequest
await ctx.integrations.slack.apiRequest(
  { method: "POST", path: "/chat.postMessage", body: { ... } },
  { response: PostMessageResponseSchema }
);
```

### Bot Token vs User Token Permissions

Different tokens have different permissions:

```typescript
// Bot tokens can:
// - Post messages to channels the bot is in
// - Read channel history (if added to channel)
// - Upload files

// User tokens can:
// - Access all channels the user can see
// - Perform actions as the user
// - Access private channels the user is in

// Check required scopes in Slack API docs for each endpoint
```

### Channel ID vs Channel Name

Most API methods require channel ID, not name:

```typescript
// WRONG - Using channel name
await ctx.integrations.slack.apiRequest(
  {
    method: "POST",
    path: "/chat.postMessage",
    body: {
      channel: "#general", // May not work
      text: "Hello",
    },
  },
  { response: schema },
);

// CORRECT - Using channel ID
await ctx.integrations.slack.apiRequest(
  {
    method: "POST",
    path: "/chat.postMessage",
    body: {
      channel: "C123456789", // Channel ID
      text: "Hello",
    },
  },
  { response: schema },
);
```

### Handling Pagination

Many endpoints return paginated results:

```typescript
async function getAllChannels(slack: SlackClient) {
  const allChannels: Channel[] = [];
  let cursor: string | undefined;

  do {
    const result = await ctx.integrations.slack.apiRequest(
      {
        method: "GET",
        path: "/conversations.list",
        params: {
          limit: 100,
          ...(cursor && { cursor }),
        },
      },
      { response: ListChannelsResponseSchema },
    );

    if (!result.ok) {
      throw new Error(`Slack API error: ${result.error}`);
    }

    allChannels.push(...result.channels);
    cursor = result.response_metadata?.next_cursor;
  } while (cursor);

  return allChannels;
}
```

### Rate Limits

Slack has rate limits (typically 1+ requests/second for most methods):

```typescript
// Slack returns 429 Too Many Requests when rate limited
// The Retry-After header indicates when to retry
```

### Message Formatting

Slack uses its own markdown variant (mrkdwn):

```typescript
const text = `
*Bold text*
_Italic text_
~Strikethrough~
\`Inline code\`
\`\`\`
Code block
\`\`\`
<https://example.com|Link text>
<@U123456789> - Mention user
<#C123456789> - Mention channel
`;
```

## Error Handling

Slack returns API errors as HTTP 200 with `{ ok: false, error: "error_code" }` instead of HTTP error status codes. The SDK surfaces these as the `SlackErrorResponse` branch of a discriminated union (`SlackResponse<T>`) — no exceptions, just check `result.ok`:

```typescript
import type { SlackResponse } from "@superblocksteam/sdk-api";

const result = await ctx.integrations.slack.apiRequest(
  { method: "POST", path: "/chat.postMessage", body: { channel, text } },
  { response: PostMessageResponseSchema },
);

if (!result.ok) {
  // TypeScript narrows to SlackErrorResponse
  console.error(`Slack error: ${result.error}`);
  //              e.g. "missing_scope", "channel_not_found", "not_authed"

  if (result.needed) {
    console.error(`Required scopes: ${result.needed}`);
    // e.g. "chat:write"
  }
  return;
}

// TypeScript narrows to T & { ok: true } — all success fields accessible
console.log(`Sent message: ${result.ts}`);
```

### SlackResponse\<T\>

Every `apiRequest` call returns `SlackResponse<T>`, a discriminated union:

| Branch  | Shape                | When                                                                       |
| ------- | -------------------- | -------------------------------------------------------------------------- |
| Success | `T & { ok: true }`   | Slack returned `ok: true`; response validated against your Zod schema      |
| Error   | `SlackErrorResponse` | Slack returned `ok: false`; contains `error`, optional `needed`/`provided` |

`T` is your payload schema output. Define payload fields only (do not include `ok` in your response schema) and the SDK injects `ok: true` for the discriminant on success.

**`SlackErrorResponse` fields:**

| Field      | Type      | Description                                                  |
| ---------- | --------- | ------------------------------------------------------------ |
| `ok`       | `false`   | Discriminant                                                 |
| `error`    | `string`  | Slack error code (e.g. `missing_scope`, `channel_not_found`) |
| `needed`   | `string?` | Required OAuth scopes (present on `missing_scope` errors)    |
| `provided` | `string?` | Current token scopes (present on `missing_scope` errors)     |

Schema validation errors (Zod mismatch on a _success_ response) still throw `RestApiValidationError`. Body validation errors also throw. Only Slack's own `ok: false` errors are returned as values.

## API Reference

- [Slack API Documentation](https://api.slack.com/methods)
- [chat.postMessage](https://api.slack.com/methods/chat.postMessage)
- [conversations.list](https://api.slack.com/methods/conversations.list)
- [OAuth Scopes](https://api.slack.com/scopes)
- [Block Kit Builder](https://app.slack.com/block-kit-builder)
