Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thread-handle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chat": minor
---

Add `chat.thread(threadId)` method to create Thread handles outside of webhook contexts
9 changes: 9 additions & 0 deletions apps/docs/content/docs/api/chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,15 @@ await dm.post("Hello via DM!");
const dm = await bot.openDM(message.author);
```

### thread

Get a Thread handle by its thread ID. Useful for posting to threads outside of webhook contexts (e.g. cron jobs, external triggers).

```typescript
const thread = bot.thread("slack:C123ABC:1234567890.123456");
await thread.post("Hello from a cron job!");
```

### channel

Get a Channel by its channel ID.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/api/thread.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Represents a conversation thread with methods for posting, subscrib
type: reference
---

A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You don't create threads directly — they come from handler callbacks or `chat.openDM()`.
A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You can also create thread handles directly using `chat.thread()` or `chat.openDM()`.

## Properties

Expand Down
27 changes: 27 additions & 0 deletions packages/chat/src/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,33 @@ describe("Chat", () => {
});
});

describe("thread", () => {
it("should return a Thread handle for a valid thread ID", () => {
const thread = chat.thread("slack:C123:1234.5678");
expect(thread).toBeDefined();
expect(thread.id).toBe("slack:C123:1234.5678");
});

it("should allow posting to a thread handle", async () => {
const thread = chat.thread("slack:C123:1234.5678");
await thread.post("Hello from outside a webhook!");
expect(mockAdapter.postMessage).toHaveBeenCalledWith(
"slack:C123:1234.5678",
"Hello from outside a webhook!"
);
});

it("should throw for an invalid thread ID", () => {
expect(() => chat.thread("")).toThrow("Invalid thread ID");
});

it("should throw for an unknown adapter prefix", () => {
expect(() => chat.thread("unknown:C123:1234.5678")).toThrow(
'Adapter "unknown" not found'
);
});
});

describe("isDM", () => {
it("should return true for DM threads", async () => {
const thread = await chat.openDM("U123456");
Expand Down
34 changes: 34 additions & 0 deletions packages/chat/src/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,40 @@ export class Chat<
});
}

/**
* Get a Thread handle by its thread ID.
*
* The adapter is automatically inferred from the thread ID prefix.
*
* @param threadId - Full thread ID (e.g., "slack:C123ABC:1234567890.123456")
* @returns A Thread that can be used to post messages, subscribe, etc.
*
* @example
* ```typescript
* const thread = chat.thread("slack:C123ABC:1234567890.123456");
* await thread.post("Hello from outside a webhook!");
* ```
*/
thread(threadId: string): Thread<TState> {
const adapterName = threadId.split(":")[0];
if (!adapterName) {
throw new ChatError(
`Invalid thread ID: ${threadId}`,
"INVALID_THREAD_ID"
);
}

const adapter = this.adapters.get(adapterName);
if (!adapter) {
throw new ChatError(
`Adapter "${adapterName}" not found for thread ID "${threadId}"`,
"ADAPTER_NOT_FOUND"
);
}

return this.createThread(adapter, threadId, {} as Message, false);
}

/**
* Infer which adapter to use based on the userId format.
*/
Expand Down
Loading