The built-in feishu source pack exposes a Feishu user's own workspace —
chats and their messages and members, tasks, and wiki spaces/nodes — as
stable SQL tables through an
Open Connector gateway. Feishu credentials are an
OAuth user_access_token obtained through the gateway's OAuth flow
against a user-provided Feishu custom app; Skardi holds only the gateway
runtime token. Visibility is exactly the authorizing user's: their chats,
their tasks, the wiki they can read.
The wire contract is Open Connector's HYBRID shape: every feishu list
executor rebuilds the pagination envelope (camelCase $.items /
$.pageToken / $.hasMore, with the provider's page_token normalized
to a null pageToken at end-of-collection) while passing the Feishu
API's item objects through raw — snake_case keys, and timestamps as
epoch digit strings (milliseconds for im, seconds for wiki), which is
what the timestamp_ms_string_utc / timestamp_s_string_utc column
types decode. Reconciled against a live gateway (v1.3.3).
Live-verified (2026-08-04): all six tables are reconciled against a real workspace end to end — registration through live discovery, real scans (86 messages over two real cursor pages, zero duplicate ids;
create_time >=pushdown narrowing a live scan; wiki's non-empty final token terminating cleanly), and every mapped column non-NULL on real rows. The gateway declares every feishu items schema loose (no declared properties), so no column is protected by the fingerprint gate — real rows are the column truth, and the bundled fixtures are redacted live captures.
spec:
data_sources:
- name: saas
type: open_connector
connection_string: http://open-connector:3000
hierarchy_level: catalog
open_connector:
runtime_token_env: OPEN_CONNECTOR_TOKEN
bindings:
# The three tables that need NO resource — a first ctx can only
# cover these; the other three each require an id you get by
# querying chats / wiki_spaces first, then coming back.
- name: team # schema name in SQL
source_pack: feishu
tables: [chats, tasks, wiki_spaces]
# Each of the remaining tables needs one resource, so each
# needs its own binding — adding them to `team` fails startup
# with `missing required resource input`.
- name: standup # per-chat binding for chat history
source_pack: feishu
resource:
containerId: oc_a1b2c3d4e5f6 # chat_id from the chats table
tables: [messages]
- name: standup_members
source_pack: feishu
resource:
chatId: oc_a1b2c3d4e5f6 # chat_id from the chats table
tables: [chat_members]
- name: handbook
source_pack: feishu
resource:
spaceId: "7034502641455497244" # space_id from wiki_spaces
tables: [wiki_nodes]SELECT name, external FROM saas.team.chats ORDER BY name;
SELECT content, create_time
FROM saas.standup.messages
WHERE create_time >= TIMESTAMP '2026-07-01T00:00:00Z'
AND msg_type = 'text'
ORDER BY create_time;
-- The same definition, ad hoc, without a binding:
SELECT member_id, name
FROM open_connector_query('saas', 'feishu.chat_members',
'{"chatId":"oc_a1b2c3d4e5f6"}');| Table | Action | Resources | Pagination | Filter pushdown |
|---|---|---|---|---|
chats |
feishu.list_chats |
— | cursor, 100/page | — |
messages |
feishu.list_messages |
containerId (required) |
cursor, 50/page | create_time >= → startTime (inexact) |
chat_members |
feishu.list_chat_members |
chatId (required) |
cursor, 100/page | — |
tasks |
feishu.list_tasks |
— | cursor, 100/page | — |
wiki_spaces |
feishu.list_wiki_spaces |
— | cursor, 50/page | — |
wiki_nodes |
feishu.list_wiki_nodes |
spaceId (required), parentNodeToken (optional) |
cursor, 50/page | — |
Design notes:
chatsandmessagespinsortType: ByCreateTimeAsc— the API's activity-ordered default reshuffles rows while a scan pages, which can skip or duplicate rows; creation order is immutable, so the cursor is stable.messagesis chat history for ONE chat (containerIdTypepinned tochat; per-chat binding, the shape Notion'sblock_childrenestablished). Thecreate_time >=pushdown renders inclusive epoch seconds as a digit string (startTime);endTimeis deliberately unmapped — it is exclusive, and flooring an upper bound would drop rows.body.contentmaps to thecontentcolumn as the provider's own JSON-encoded payload text (its inner schema varies bymsg_type).taskssendstype: my_tasks— the only value Feishu accepts (1470400: Invalid Param 'type'. Only 'my_tasks' is supported.forassigned/created/followed), not a tunable choice — and always omits thecompletedinput, Feishu's spelling of a state=all listing. Nothing pushes it: real rows carry nocompletedboolean (completion on the wire isstatus: todo|donepluscompleted_at), so filter onstatuslocally.wiki_nodeslists ONE level: the children ofparentNodeToken, or the space root when omitted. Walking a whole space is client-side recursion overhas_child/node_token.- No table declares
error_path: the gateway's executors consume Feishu's in-bandcode != 0envelope and return a failure envelope themselves.
The gateway's feishu provider uses the OAuth authorization-code flow
(authTypes: ["oauth2"]) against a Feishu custom app the operator
creates. Rows are the authorizing user's view — a chat the user left or
a wiki space they cannot read is simply absent, not an error.
Gateway version is a floor, not a fact: the six actions this pack
needs were added to Open Connector after older mid-2025 builds (which
expose only docs/bitable feishu actions); a too-old gateway fails
registration with action 'feishu.list_chats' was not found, which
reads like a typo but means "upgrade the gateway". Self-check before
going further:
curl -s -H "Authorization: Bearer $OPEN_CONNECTOR_TOKEN" \
"$GATEWAY/v1/actions?service=feishu&limit=500" \
| python3 -c "import json,sys; d=json.load(sys.stdin); \
ids=[i['id'] for i in d['data']['items']]; print(len(ids)); \
print([n for n in ['feishu.list_chats','feishu.list_messages','feishu.list_chat_members','feishu.list_tasks','feishu.list_wiki_spaces','feishu.list_wiki_nodes'] if n not in ids])"
# expect: a few hundred actions, then an empty list []Operational findings from the live verification, all three of which the Feishu console gates independently of each other:
-
The
messagestable requires theim:message:readonlyscope (orim:message/im:message.history:readonly) — theim:message.*.get_as_userscopes the gateway's action metadata declares are NOT honored for the user-identity read path (Feishu 99991679 names the real set). -
The im tables additionally require the app's bot capability (Feishu 232025), even though every read runs as the user.
-
Feishu's
im/v1/messagescapspage_sizeat 50 on the wire (99992402 above it) despite the gateway schema declaring 100 — the pack requests 50. -
Upstream gateway caveat: its authorization URL requests the union of ALL feishu actions' scopes with no narrowing surface — measured at 164 scopes on the live authorize URL, including destructive write scopes, to read six tables — which Feishu rejects (20027) unless the app enables every one. Until upstream grows a config-level override (#267), narrow
feishuOAuthScopesin the gateway'ssrc/providers/feishu/definition.tsto what these tables need:const feishuOAuthScopes = [ "offline_access", "im:chat:read", // chats "im:chat.members:read", // chat_members "im:message:readonly", // messages (next three also messages) "im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "im:message.reactions:read", "task:task:read", // tasks "wiki:space:retrieve", // wiki_spaces "wiki:node:retrieve", // wiki_nodes ];
The Feishu console bulk-imports scopes, so enabling them is one paste:
{"scopes":{"tenant":[],"user":["offline_access","im:chat:read","im:chat.members:read", "im:message:readonly","im:message.group_msg:get_as_user","im:message.p2p_msg:get_as_user", "im:message.reactions:read","task:task:read","wiki:space:retrieve","wiki:node:retrieve"]}} -
Zero-trust corporate VPNs (aTrust / EasyConnect class) that map external domains into
198.18.0.0/15trip the gateway's egress guard BEFORE any request leaves the process: the OAuth exchange fails in tens of milliseconds with onlyoauth_token_exchange_failedand no resolved address — the speed is the tell. The guard checks reserved ranges unconditionally (OOMOL_CONNECT_ALLOW_PRIVATE_NETWORKcannot open them, and it does not reach provider egress at all); tracked upstream as #275.
Filed upstream (oomol-lab/open-connector) from this verification pass: #267 (scope-union OAuth URL), #268 (get_as_user scopes not honored for user-identity reads), #269 (declared pageSize 100 vs the wire's 50 cap, fix in PR #271), and #270 (wiki's non-empty final page_token beside has_more:false).
message_position (a digit string on every live message row) is
deliberately unmapped: no public Feishu documentation pins its
semantics.
Feishu applies per-app and per-user QPS limits; the client's bounded
retry/backoff handles transient 429/5xx envelopes. Scans fetch pages on
demand and stop early under LIMIT; completed scans are cached per the
scan cache's usual keying (binding, table, pushed inputs, LIMIT).