The feishu pack can enumerate documents but cannot read them. The gateway has the actions; the pack layer cannot express their shape. Evidence below is pinned to SkardiLabs/skardi@2291b19 and oomol-lab/open-connector@bf94ce8.
The gap
The feishu pack (merged 2026-08-06, 2291b19) exposes six tables: chats,
messages, chat_members, tasks, wiki_spaces, wiki_nodes. That covers
chat history well. It does not cover document text at all: wiki_nodes yields
node_token / obj_token / obj_type / title / url — the keys you need to
open a document, but no column carrying what the document says.
So today an agent can answer "what was discussed in this chat" but not
"what does this document say", even though the second is the request users
keep making.
This is not an upstream gap. The gateway has the actions. Verified against
oomol-lab/open-connector at bf94ce8, and the feishu provider registers every
action group (src/providers/feishu/actions.ts:201-240), so all of these exist
on a current gateway:
| Action |
Response shape |
Mappable as a table today? |
feishu.get_document_content |
single object {documentId, content} — full plain text (actions.ts:94) |
No |
feishu.fetch_document |
single object {document: {…}}, with scope / detail / keyword / block-range reads (shared/docs-actions.ts:91, schema at :28) |
No |
feishu.list_document_blocks |
array, same envelope as the six mapped tables (actions.ts:112) |
Yes, but see below |
feishu.search_documents |
array {results, hasMore, pageToken} (shared/docs-actions.ts:192) |
Yes — metadata only, no text |
Why the two content actions cannot be mapped
A pack table locates its rows with row_path, and that path must resolve to an
array:
RowPath::parse rejects $ outright — "no segments"
(crates/skardi/src/sources/providers/open_connector/row_path.rs:31).
RowPath::rows fails with RowPathNotArray on a non-array target
(row_path.rs:98).
Both content actions return one object at the root. There is no nested array to
point at, so no row_path can express them.
Why list_document_blocks does not close the gap either
It is array-shaped and needs no engine change — but a Feishu docx block carries
its payload under a key named by the block's own type (text, heading1 …
heading9, bullet, ordered, code, quote, …). A fixed relational mapping
cannot address a key whose name varies per row.
This is exactly the wall the Notion pack already hit, and the note there records
the decision taken at the time:
The type-specific payload (paragraph/heading_1/...) lives under a key named BY
type, which a fixed relational mapping cannot address; content extraction
belongs to a rendered-markdown table (notion.retrieve_page_markdown) as a
follow-up.
— packs/notion.yaml:110
Mapping list_document_blocks is still possible and still an improvement —
structure, hierarchy, per-block edit times, and the raw block object as a json
column that SQL can dig into. But "dig the text out with JSON functions" is not
the same product as a content column, and the Notion note suggests the project
already judged this route insufficient for content.
What would close it
An optional, per-table declaration that the response is one row — everything
else unchanged. Sketch:
document_content:
action: feishu.get_document_content
row_shape: object # new; default `array` = today's behaviour
row_path: "$" # only legal when row_shape is object
pagination: { strategy: single_page }
resources:
required: [documentId]
columns:
- { name: document_id, path: documentId, type: utf8, nullable: false }
- { name: content, path: content, type: utf8, nullable: true }
Surface of the change, measured:
TableDoc gains one #[serde(default)] field
(packs/loader.rs, struct TableDoc).
RowPath needs to accept a root path when the shape is object
(row_path.rs:31 is the only rule in the way).
- One production call site consumes rows —
exec.rs:509 (let rows = self.row_path.rows(&envelope, page)?;). Every other
.rows( occurrence in the tree is inside pack test modules.
PaginationStrategy::SinglePage already exists (pagination.rs:83), so the
no-pagination half needs nothing new.
- Default is today's behaviour, so the four existing packs are untouched — no
yaml change, no behaviour change.
Why this may be worth more than one table
The Notion follow-up quoted above hits the same wall: a rendered-content action
returns one document, not a page of rows. If the row model learns single-object
rows once, both providers can map content, and any future provider whose
"read one thing" action matters gets it for free.
What I am NOT proposing
- Not proposing to write this myself and hand you a fait accompli. The Notion
note tells me this wall was met and deliberately deferred once already, which
suggests you may have a different mechanism in mind (a rendered-content
concept? a deliberate choice to keep the model scan-only?). That decision is
yours; I would rather ask than push engine code through review.
- Not proposing anything about writes. All four actions above are reads.
Open questions for you
- Is a single-row table acceptable in a scan-oriented model, or is that the
reason content extraction was deferred on Notion?
- If yes — is
row_shape: object the shape you want, or would you rather the
gateway grow a list-shaped content action so the pack layer stays as is?
- If no — is mapping
list_document_blocks with a raw-block json column
worth doing as the interim, knowing the text still needs JSON digging?
What I can and cannot verify
I can implement and unit-test the row-model change locally: it needs no gateway.
I cannot produce a submission-ready table. Per the source-pack skill's gate, a
new table needs a fingerprint captured from a live gateway and a phase-4 pass
where real rows settle column truth. I have no gateway running and no clean
Feishu app to authorize (the app I can authorize already carries 133 scopes from
other tools, so it cannot establish a minimum-scope set either). Whoever has the
live setup would need to finish those two steps.
@bakey the three open questions above are for you — particularly (1), since the Notion note suggests this wall was met and deferred deliberately rather than overlooked. Happy to implement the row-model half if you want it; happy to drop it if you have a different mechanism in mind.
The
feishupack can enumerate documents but cannot read them. The gateway has the actions; the pack layer cannot express their shape. Evidence below is pinned toSkardiLabs/skardi@2291b19andoomol-lab/open-connector@bf94ce8.The gap
The
feishupack (merged 2026-08-06,2291b19) exposes six tables:chats,messages,chat_members,tasks,wiki_spaces,wiki_nodes. That coverschat history well. It does not cover document text at all:
wiki_nodesyieldsnode_token/obj_token/obj_type/title/url— the keys you need toopen a document, but no column carrying what the document says.
So today an agent can answer "what was discussed in this chat" but not
"what does this document say", even though the second is the request users
keep making.
This is not an upstream gap. The gateway has the actions. Verified against
oomol-lab/open-connectoratbf94ce8, and the feishu provider registers everyaction group (
src/providers/feishu/actions.ts:201-240), so all of these existon a current gateway:
feishu.get_document_content{documentId, content}— full plain text (actions.ts:94)feishu.fetch_document{document: {…}}, withscope/detail/keyword/ block-range reads (shared/docs-actions.ts:91, schema at:28)feishu.list_document_blocksactions.ts:112)feishu.search_documents{results, hasMore, pageToken}(shared/docs-actions.ts:192)Why the two content actions cannot be mapped
A pack table locates its rows with
row_path, and that path must resolve to anarray:
RowPath::parserejects$outright — "no segments"(
crates/skardi/src/sources/providers/open_connector/row_path.rs:31).RowPath::rowsfails withRowPathNotArrayon a non-array target(
row_path.rs:98).Both content actions return one object at the root. There is no nested array to
point at, so no
row_pathcan express them.Why
list_document_blocksdoes not close the gap eitherIt is array-shaped and needs no engine change — but a Feishu docx block carries
its payload under a key named by the block's own type (
text,heading1…heading9,bullet,ordered,code,quote, …). A fixed relational mappingcannot address a key whose name varies per row.
This is exactly the wall the Notion pack already hit, and the note there records
the decision taken at the time:
Mapping
list_document_blocksis still possible and still an improvement —structure, hierarchy, per-block edit times, and the raw block object as a
jsoncolumn that SQL can dig into. But "dig the text out with JSON functions" is not
the same product as a
contentcolumn, and the Notion note suggests the projectalready judged this route insufficient for content.
What would close it
An optional, per-table declaration that the response is one row — everything
else unchanged. Sketch:
Surface of the change, measured:
TableDocgains one#[serde(default)]field(
packs/loader.rs, structTableDoc).RowPathneeds to accept a root path when the shape isobject(
row_path.rs:31is the only rule in the way).exec.rs:509(let rows = self.row_path.rows(&envelope, page)?;). Every other.rows(occurrence in the tree is inside pack test modules.PaginationStrategy::SinglePagealready exists (pagination.rs:83), so theno-pagination half needs nothing new.
yaml change, no behaviour change.
Why this may be worth more than one table
The Notion follow-up quoted above hits the same wall: a rendered-content action
returns one document, not a page of rows. If the row model learns single-object
rows once, both providers can map content, and any future provider whose
"read one thing" action matters gets it for free.
What I am NOT proposing
note tells me this wall was met and deliberately deferred once already, which
suggests you may have a different mechanism in mind (a rendered-content
concept? a deliberate choice to keep the model scan-only?). That decision is
yours; I would rather ask than push engine code through review.
Open questions for you
reason content extraction was deferred on Notion?
row_shape: objectthe shape you want, or would you rather thegateway grow a list-shaped content action so the pack layer stays as is?
list_document_blockswith a raw-blockjsoncolumnworth doing as the interim, knowing the text still needs JSON digging?
What I can and cannot verify
I can implement and unit-test the row-model change locally: it needs no gateway.
I cannot produce a submission-ready table. Per the source-pack skill's gate, a
new table needs a
fingerprintcaptured from a live gateway and a phase-4 passwhere real rows settle column truth. I have no gateway running and no clean
Feishu app to authorize (the app I can authorize already carries 133 scopes from
other tools, so it cannot establish a minimum-scope set either). Whoever has the
live setup would need to finish those two steps.
@bakey the three open questions above are for you — particularly (1), since the Notion note suggests this wall was met and deferred deliberately rather than overlooked. Happy to implement the row-model half if you want it; happy to drop it if you have a different mechanism in mind.