| name | sandbox-notion |
| description | Call the Notion API from a Code node or CodeAct action, with NodeTool's request builder and rich-text/block readers running on the host |
Notion in the sandbox
Specifier: @nodetool-ai/sandbox-notion. Import it at the top of the body.
Notion is an ordinary HTTPS API and fetch reaches it. What costs a script its
first three attempts is everything around the call — the Notion-Version
header, the rich-text arrays every piece of text is wrapped in, and the block
tree a page's content comes back as. Those three are this pack.
Nothing here sends a request. request builds one; the guest's own fetch
sends it, under the run's fetch cap and SSRF guard.
request — an authenticated Notion request
import { request } from "@nodetool-ai/sandbox-notion";
const token = await nodetool.secrets.get("NOTION_API_KEY");
const req = await request({
token,
path: "search",
method: "POST",
body: { query: inputs.query, page_size: 10 }
});
const res = await fetch(req.url, req);
if (!res.ok) throw new Error(`Notion search failed: ${res.status}`);
return { results: res.json.results };
Options: token (required), path (required — relative to /v1 unless it
starts with /), method (default GET), body (JSON-encoded when present),
query (query-string bag), version (default 2022-06-28).
Returns {url, method, headers, body?}, which fetch(req.url, req) takes
whole.
plainText — a rich-text array as a string
import { plainText } from "@nodetool-ai/sandbox-notion";
const title = await plainText(page.properties.Name.title);
Takes the array Notion puts under every title, rich_text and heading.
Anything else gives "".
toMarkdown — a page's blocks as markdown
import { request, toMarkdown } from "@nodetool-ai/sandbox-notion";
const req = await request({ token, path: `blocks/${inputs.page_id}/children`, query: { page_size: 100 } });
const blocks = (await fetch(req.url, req)).json.results;
return { markdown: await toMarkdown(blocks) };
Handles paragraphs, the three heading levels, bulleted and numbered items,
to-dos, quotes, code fences and dividers. A block markdown has no form for —
a database, an embed, a synced block — contributes nothing rather than a
placeholder, so the result is text a model or a file can take as-is.
Common flows
Create a page in a database
const req = await request({
token, path: "pages", method: "POST",
body: {
parent: { database_id: inputs.database_id },
properties: { Name: { title: [{ text: { content: inputs.title } }] } }
}
});
const page = (await fetch(req.url, req)).json;
Query a database
const req = await request({
token, path: `databases/${inputs.database_id}/query`, method: "POST",
body: { filter: { property: "Status", select: { equals: "Open" } }, page_size: 50 }
});
Gotchas
- Every export is async. A host call is a round trip.
- Nested blocks are not fetched.
toMarkdown is pure over the blocks you
hand it. A block with has_children needs its own
blocks/{id}/children call.
- Notion paginates at 100. Follow
next_cursor with
query: { start_cursor } and watch the run's fetch cap.
- The integration needs access. A page shared with nobody returns 404 even
with a valid token — share it with the integration in Notion first.