Skip to main content
Blog
PreviousNext

Coral’s Slack Connector Now Uses Table Functions: What Changed and Why

If you’ve been using Coral to query Slack data and noticed that slack.messages stopped working as a table, here's what happened. In Coral 0.3.0, channel messages and thread replies moved from filtered tables to table functions. This post covers exactly what changed, why the Coral team made this decision, and how to update your queries.

What Is Coral?

Coral is an open-source SQL runtime that lets you query APIs and databases as regular SQL tables. You connect a source (Slack, GitHub, Linear, Notion, etc.) and query it with standard SQL, locally, without any data leaving your machine.

What Changed in 0.3.0

Before 0.3.0, slack.messages was a table:

SELECT text, ts, user_id
FROM slack.messages
WHERE channel = 'C0123456789'
AND oldest = '1716000000'
AND latest = '1716400000'

That WHERE channel = ... filter was required. Without it, the query would fail or return nothing. But nothing in the schema told you that. It looked like a normal table you could SELECT * from freely.

Starting in 0.3.0, slack.messages and slack.thread_replies are table functions. The required arguments are part of the call signature:

SELECT text, ts, user_id
FROM slack.messages(channel => 'C0123456789')

slack.channels and slack.users are unchanged. They still work as regular tables with no required filters.

Why Table Functions?

The main driver was agent reliability.

When an agent sees slack.messages as a table, it writes SELECT * FROM slack.messages LIMIT 10. The query fails because channel is required but the schema doesn't say that anywhere. The agent backtracks, re-reads the schema hints, figures out the filter, and retries. That's 1 to 2 extra turns wasted on a completely avoidable error.

With a function signature, the argument is explicit in the schema. The agent sees slack.messages(channel => ...) and writes the correct query on the first attempt.

Small API change, real impact on agent accuracy.

Discovering Available Table Functions

To see all table functions registered in your Coral instance:

SELECT * FROM coral.table_functions
WHERE schema_name = 'slack'

To see all sources and what they expose:

SELECT schema_name, table_name
FROM coral.tables

The New Syntax

slack.messages: Channel Messages

SELECT ts, user_id, text, thread_ts, reply_count, subtype
FROM slack.messages(channel => 'C0123456789')
ORDER BY ts DESC
LIMIT 50

messages() Arguments

  • channel (required): Slack channel ID, e.g. C0123456789
  • oldest (optional): Return messages after this Unix timestamp, in seconds, as a string
  • latest (optional): Return messages before this Unix timestamp, in seconds, as a string

messages() Columns

  • user_id — Utf8, nullable: User ID of the message author
  • text — Utf8, nullable: Message text content
  • ts — Timestamp, not null: Message time as UTC Timestamp
  • thread_ts — Utf8, nullable: Thread parent timestamp, null if not in a thread
  • reply_count — Int64, nullable: Number of replies in thread
  • subtype — Utf8, nullable: Message subtype, null for regular messages

ts is a proper Timestamp column now, not a float string. You can use it directly in comparisons and date functions without any casting.

slack.thread_replies: Thread Replies

SELECT ts, user_id, text
FROM slack.thread_replies(
channel => 'C0123456789',
thread_ts => '1716400000.000000'
)
ORDER BY ts ASC

The thread_ts value to pass here is the thread_ts column from a slack.messages row.

thread_replies() Arguments

  • channel (required): Slack channel ID
  • thread_ts (required): Parent message timestamp
  • oldest (optional): Return replies after this Unix timestamp
  • latest (optional): Return replies before this Unix timestamp
  • inclusive (optional): Include messages at oldest/latest boundaries
  • include_all_metadata (optional): Return full metadata on each reply

thread_replies() Columns

  • thread_ts — Utf8, nullable: Thread parent timestamp
  • user_id — Utf8, nullable: User ID of the reply author
  • text — Utf8, nullable: Reply text content
  • ts — Timestamp, not null: Reply time as UTC Timestamp
  • reply_count — Int64, nullable: Number of replies in thread
  • subtype — Utf8, nullable: Message subtype

Tables That Did Not Change

slack.channels and slack.users are still regular tables, no function call needed.

slack.channels

SELECT id, name, topic, purpose, num_members
FROM slack.channels
WHERE is_archived IS NOT TRUE
ORDER BY num_members DESC

channels Columns

  • id — Utf8, not null: Channel ID
  • name — Utf8, not null: Channel name
  • topic — Utf8, nullable: Channel topic text
  • purpose — Utf8, nullable: Channel purpose text
  • num_members — Int64, nullable: Number of members
  • is_archived — Boolean, not null: Whether the channel is archived
  • created — Int64, not null: Unix timestamp when the channel was created

slack.users

SELECT id, name, real_name, email
FROM slack.users
WHERE is_bot IS NOT TRUE
AND deleted IS NOT TRUE

users Columns

  • id — Utf8, not null: User ID
  • name — Utf8, not null: Username
  • real_name — Utf8, nullable: Full name
  • display_name — Utf8, nullable: Display name in Slack
  • email — Utf8, nullable: Email address (requires users:read.email scope)
  • is_bot — Boolean, not null: Whether the user is a bot
  • is_admin — Boolean, nullable: Whether the user is a workspace admin
  • deleted — Boolean, nullable: Whether the account is deactivated

Required OAuth Scopes

Your Slack app needs these Bot Token scopes:

  • channels:read: List public channels
  • channels:history: Read messages in public channels
  • groups:read: List private channels
  • groups:history: Read messages in private channels
  • users:read: List workspace users
  • users:read.email: Read user email addresses (optional, needed for the email column)

Migrating Existing Queries

Before:

FROM slack.messages
WHERE channel = 'C0123456789'
AND oldest = '1716000000'
AND latest = '1716400000'

After:

FROM slack.messages(
channel => 'C0123456789',
oldest => '1716000000',
latest => '1716400000'
)

oldest and latest still work the same way: Unix timestamps in seconds, passed as strings. They're just function arguments now instead of WHERE filters.

If you were casting ts from a float in your old queries, remove that cast. ts is already a Timestamp:

Before:

ON to_timestamp(CAST(sl.ts AS DOUBLE)) >= CAST(pd.created_at AS TIMESTAMP)

After:

ON sl.ts >= CAST(pd.created_at AS TIMESTAMP)

Example: Messages in a Time Range

oldest and latest are passed directly to the Slack API as Unix timestamp strings. Compute them in your application and pass them as literals:

SELECT ts, user_id, text, reply_count
FROM slack.messages(
channel => 'C0123456789',
oldest => '1716000000',
latest => '1716800000'
)
WHERE subtype IS NULL
ORDER BY ts DESC

In Python:

import time
oldest = str(int(time.time()) - 7 * 24 * 60 * 60) # 7 days ago
latest = str(int(time.time()))

Example: Find Active Threads

Get messages that have replies, then fetch the thread:

-- Step 1: find messages with replies
SELECT ts, thread_ts, user_id, text, reply_count
FROM slack.messages(channel => 'C0123456789')
WHERE reply_count > 0
ORDER BY ts DESC
LIMIT 10;
-- Step 2: fetch a specific thread using the thread_ts value from above
SELECT ts, user_id, text
FROM slack.thread_replies(
channel => 'C0123456789',
thread_ts => '1716400000.000000'
)
ORDER BY ts ASC;

Summary of Changes

slack.messages: was a table with a required hidden WHERE filter, now a table function slack.messages(channel => '...')

slack.thread_replies: was a table with required hidden WHERE filters, now a table function slack.thread_replies(channel => '...', thread_ts => '...')

slack.channels: unchanged, still a regular table

slack.users: unchanged, still a regular table

ts column: was a float string (seconds since epoch), now a proper Timestamp (UTC)

If you have existing queries using the old table syntax, the migration is straightforward. Move the WHERE filters into function arguments and drop any to_timestamp(CAST(ts AS DOUBLE)) casts while you're at it.

Related Posts

View all posts →

Rajdeep
Singhio

Full Stack Developer & AI Engineer

© Copyright

RJDP-2026Built with Next.js & Tailwind