Skip to main content

Overview

Gorkie integrates deeply with Slack using the Bolt SDK. It can send messages, add reactions, read conversation history, look up user information, and manage channels.

Message Replies

Gorkie’s primary way of communicating is through message replies.

Reply Tool

From server/lib/ai/tools/chat/reply.ts:70-98:
export const reply = ({
  context,
  stream,
}: {
  context: SlackMessageContext;
  stream: Stream;
}) =>
  tool({
    description:
      'Send messages to the Slack channel. Use type "reply" to respond in a thread or "message" for the main channel.',
    inputSchema: z.object({
      offset: z
        .number()
        .int()
        .min(0)
        .optional()
        .describe(
          `Number of messages to go back from the triggering message. 0 or omitted means that you will reply to the message that you were triggered by.`
        ),
      content: z
        .array(z.string())
        .nonempty()
        .describe('An array of lines of text to send. Send at most 4 lines.')
        .max(4),
      type: z
        .enum(['reply', 'message'])
        .default('reply')
        .describe('Reply in a thread or post directly in the channel.'),
    }),
    // ... execution logic
  });

Reply Parameters

  • content - Array of message lines (max 4)
  • type - "reply" (thread) or "message" (channel)
  • offset - How many messages back to reply to (default: 0)

Thread vs Channel Messages

From server/lib/ai/tools/chat/reply.ts:128-134:
const target = await resolveTargetMessage(context, offset, ctxId);
const threadTs =
  type === 'reply'
    ? resolveThreadTs(target, currentThread ?? messageTs)
    : undefined;

for (const text of content) {
  await context.client.chat.postMessage({
    channel: channelId,
    markdown_text: text,
    thread_ts: threadTs,
  });
}
Message types:
  • reply - Posts in a thread (creates thread if needed)
  • message - Posts directly in the channel
Gorkie uses markdown_text for message formatting, so you can use standard Markdown in your messages.

Offset Feature

The offset parameter allows Gorkie to reply to earlier messages. From server/lib/ai/tools/chat/reply.ts:10-52:
async function resolveTargetMessage(
  ctx: SlackMessageContext,
  offset: number,
  ctxId: string
): Promise<SlackHistoryMessage | null> {
  const channelId = ctx.event.channel;
  const messageTs = ctx.event.ts;

  if (!(channelId && messageTs)) {
    return null;
  }

  if (offset <= 0) {
    return {
      ts: messageTs,
      thread_ts: ctx.event.thread_ts,
    };
  }

  const history = await ctx.client.conversations.history({
    channel: channelId,
    latest: messageTs,
    inclusive: false,
    limit: offset,
  });

  const sorted: SlackHistoryMessage[] = (history.messages ?? [])
    .filter(
      (msg): msg is { thread_ts?: string; ts: string } =>
        typeof msg.ts === 'string'
    )
    .sort((a, b) => Number(b.ts) - Number(a.ts))
    .map((msg) => ({
      ts: msg.ts,
      thread_ts: msg.thread_ts,
    }));

  return sorted[offset - 1] ?? { ts: messageTs };
}
  • offset: 0 - Reply to the triggering message (default)
  • offset: 1 - Reply to the message before the trigger
  • offset: 2 - Reply two messages back

Reactions

Gorkie can add emoji reactions to messages. From server/lib/ai/tools/chat/react.ts:8-32:
export const react = ({
  context,
  stream,
}: {
  context: SlackMessageContext;
  stream: Stream;
}) =>
  tool({
    description:
      'Add emoji reactions to the current Slack message. Provide emoji names without surrounding colons.',
    inputSchema: z.object({
      emojis: z
        .array(z.string().min(1))
        .nonempty()
        .describe('Emoji names to react with (unicode or custom names).'),
    }),
    onInputStart: async ({ toolCallId }) => {
      await createTask(stream, {
        taskId: toolCallId,
        title: 'Adding reaction',
        status: 'pending',
      });
    },
    execute: async ({ emojis }, { toolCallId }) => {
      // ... execution logic
    },
  });

Adding Reactions

From server/lib/ai/tools/chat/react.ts:51-58:
for (const emoji of emojis) {
  await context.client.reactions.add({
    channel: channelId,
    name: emoji.replace(/:/g, ''),
    timestamp: messageTs,
  });
}
Usage:
@gorkie react with thumbsup and eyes
Gorkie will add 👍 and 👀 reactions to your message.
You can use both standard Unicode emoji names (like “thumbsup”) and custom workspace emoji names.

User Information

Gorkie can look up detailed information about Slack users. From server/lib/ai/tools/chat/get-user-info.ts:17-31:
export const getUserInfo = ({
  context,
  stream,
}: {
  context: SlackMessageContext;
  stream: Stream;
}) =>
  tool({
    description: 'Get details about a Slack user by ID.',
    inputSchema: z.object({
      userId: z
        .string()
        .min(1)
        .describe('The Slack user ID (e.g. U123) of the user.'),
    }),
    // ... execution logic
  });

User Data Returned

From server/lib/ai/tools/chat/get-user-info.ts:56-72:
return {
  success: true,
  data: {
    id: user.id,
    username: user.name,
    displayName: user.profile?.display_name,
    realName: user.profile?.real_name,
    statusText: user.profile?.status_text,
    statusEmoji: user.profile?.status_emoji,
    isBot: user.is_bot,
    tz: user.tz,
    updated: user.updated,
    title: user.profile?.title,
    teamId: user.team_id,
    idResolved: targetId ?? null,
  },
};
Available data:
  • User ID and username
  • Display name and real name
  • Status text and emoji
  • Whether they’re a bot
  • Timezone
  • Job title
  • Team ID

Conversation History

Gorkie can read message history from public channels and threads. From server/lib/ai/tools/chat/read-conversation-history.ts:17-52:
export const readConversationHistory = ({
  context,
  stream,
}: {
  context: SlackMessageContext;
  stream: Stream;
}) =>
  tool({
    description:
      'Read message history from a public Slack channel or thread using a channel ID and an optional thread timestamp.',
    inputSchema: z.object({
      channelId: z
        .string()
        .default(context.event.channel ?? '')
        .describe('Target Slack channel ID.'),
      threadTs: z
        .string()
        .optional()
        .describe(
          'Optional thread timestamp. Use this to read a specific thread.'
        ),
      limit: z
        .number()
        .int()
        .min(1)
        .max(200)
        .default(40)
        .describe('Maximum number of messages to return (1-200).'),
      latest: z
        .string()
        .optional()
        .describe('Optional upper timestamp bound for returned messages.'),
      oldest: z
        .string()
        .optional()
        .describe('Optional lower timestamp bound for returned messages.'),
      inclusive: z
        .boolean()
        .default(false)
        .describe(
          'When true, include messages exactly at latest/oldest boundaries.'
        ),
    }),
    // ... execution logic
  });

Privacy Protection

Gorkie blocks access to private conversations: From server/lib/ai/tools/chat/read-conversation-history.ts:98-119:
const isPrivateConversation =
  channel.is_private ||
  channel.is_im ||
  channel.is_mpim ||
  channel.is_group;
if (isPrivateConversation) {
  const message =
    'Reading private conversations is not allowed. Use a public channel instead.';
  logger.warn(
    { ctxId, channelId },
    'Blocked private conversation read'
  );
  await finishTask(stream, {
    status: 'error',
    taskId: task,
    output: message,
  });
  return {
    success: false,
    error: message,
  };
}
Gorkie can only read history from public channels. Private channels, DMs, and group messages are blocked for privacy.

Channel Operations

Gorkie automatically joins channels when needed. From server/slack/conversations.ts:15-32:
async function joinChannel(
  client: ConversationOptions['client'],
  channel: string
): Promise<void> {
  try {
    // keep previous behavior: best-effort join and swallow failures
    await fetch('https://slack.com/api/conversations.join', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${client.token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ channel }),
    });
  } catch {
    // this is fine - channel may not support join
  }
}
This happens automatically before fetching messages. If the join fails (e.g., already a member or channel doesn’t support joining), Gorkie continues anyway.

Message Context Building

When Gorkie processes a message, it builds rich context. From server/slack/conversations.ts:203-241:
export async function getConversationMessages({
  client,
  channel,
  threadTs,
  botUserId,
  limit = 40,
  latest,
  oldest,
  inclusive = false,
}: ConversationOptions): Promise<ModelMessage[]> {
  try {
    const mentionRegex = botUserId ? new RegExp(`<@${botUserId}>`, 'gi') : null;
    const messages = await fetchMessages({
      client,
      channel,
      threadTs,
      botUserId,
      limit,
      latest,
      oldest,
      inclusive,
    });
    const filteredMessages = filterMessages(messages, latest, inclusive);
    const userCache = await buildUserCache(client, filteredMessages);
    const sortedMessages = sortForModel(filteredMessages);

    return await Promise.all(
      sortedMessages.map((message) =>
        toModelMessage(message, { botUserId, mentionRegex, userCache })
      )
    );
  } catch (error) {
    logger.error(
      { ...toLogError(error), channel, threadTs },
      'Failed to fetch conversation history'
    );
    return [];
  }
}

Message Formatting

From server/slack/conversations.ts:148-201:
async function toModelMessage(
  message: SlackConversationMessage,
  options: {
    botUserId?: string;
    mentionRegex: RegExp | null;
    userCache: Map<string, CachedUser>;
  }
): Promise<ModelMessage> {
  const { botUserId, mentionRegex, userCache } = options;

  const isAssistantMessage =
    message.user === botUserId || Boolean(message.bot_id);
  const original = message.text ?? '';
  const cleaned = mentionRegex
    ? original.replace(mentionRegex, '').trim()
    : original.trim();
  const textContent = cleaned.length > 0 ? cleaned : original;

  const author = message.user
    ? (userCache.get(message.user)?.displayName ?? message.user)
    : (message.bot_id ?? 'unknown');
  const authorId = message.user ?? message.bot_id ?? 'unknown';

  const formattedText = `${author} (${authorId}): ${textContent}`;

  if (isAssistantMessage) {
    return {
      role: 'assistant' as const,
      content: formattedText,
    };
  }

  const imageContents = await processSlackFiles(message.files);
  if (imageContents.length > 0) {
    const contentParts: UserContent = [
      {
        type: 'text' as const,
        text: formattedText,
      },
      ...imageContents,
    ];

    return {
      role: 'user' as const,
      content: contentParts,
    };
  }

  return {
    role: 'user' as const,
    content: formattedText,
  };
}
Message processing:
  1. Removes bot mentions (e.g., <@U123>)
  2. Resolves user display names
  3. Formats as display-name (user-id): text
  4. Attaches image files if present
  5. Labels messages as assistant or user role

Best Practices

  1. Use threads - Keep conversations organized
  2. Public channels - Gorkie works best in public channels
  3. Clear mentions - Use @gorkie to get attention
  4. Be patient - Some operations take time
When asking Gorkie to react or reply to a specific message, describe which message clearly. Gorkie can use the offset parameter to target the right message.

Build docs developers (and LLMs) love