roam_create_page
Create a new standalone page in Roam Research with structured outlines using explicit nesting levels and headings (H1-H3). This tool establishes foundational concept pages, new topic areas, reference materials, or permanent information collections in a single step.
Instructions
Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:
Creating foundational concept pages that other pages will link to/from
Establishing new topic areas that need their own namespace
Setting up reference materials or documentation
Making permanent collections of information. IMPORTANT: Before using this tool, ensure that you have loaded into context the 'Roam Markdown Cheatsheet' resource.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Title of the new page | |
| content | No | Initial content for the page as an array of blocks with explicit nesting levels. Note: While empty blocks (e.g., {"text": "", "level": 1}) can be used for visual spacing, they create empty entities in the database. Please use them sparingly and only for structural purposes, not for simple visual separation. |
Implementation Reference
- src/tools/operations/pages.ts:97-222 (handler)Core handler function that creates a new Roam page (or uses existing), adds structured content via batch actions using convertToRoamActions, and optionally links it to the daily page.async createPage(title: string, content?: Array<{ text: string; level: number; heading?: number }>): Promise<{ success: boolean; uid: string }> { // Ensure title is properly formatted const pageTitle = String(title).trim(); // First try to find if the page exists const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; type FindResult = [string]; const findResults = await q(this.graph, findQuery, [pageTitle]) as FindResult[]; let pageUid: string | undefined; if (findResults && findResults.length > 0) { // Page exists, use its UID pageUid = findResults[0][0]; } else { // Create new page try { await createRoamPage(this.graph, { action: 'create-page', page: { title: pageTitle } }); // Get the new page's UID const results = await q(this.graph, findQuery, [pageTitle]) as FindResult[]; if (!results || results.length === 0) { throw new Error('Could not find created page'); } pageUid = results[0][0]; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create page: ${error instanceof Error ? error.message : String(error)}` ); } } // If content is provided, create blocks using batch operations if (content && content.length > 0) { try { // Convert content array to MarkdownNode format expected by convertToRoamActions const nodes = content.map(block => ({ content: convertToRoamMarkdown(block.text.replace(/^#+\s*/, '')), level: block.level, ...(block.heading && { heading_level: block.heading }), children: [] })); // Create hierarchical structure based on levels const rootNodes: any[] = []; const levelMap: { [level: number]: any } = {}; for (const node of nodes) { if (node.level === 1) { rootNodes.push(node); levelMap[1] = node; } else { const parentLevel = node.level - 1; const parent = levelMap[parentLevel]; if (!parent) { throw new Error(`Invalid block hierarchy: level ${node.level} block has no parent`); } parent.children.push(node); levelMap[node.level] = node; } } // Generate batch actions for all blocks const actions = convertToRoamActions(rootNodes, pageUid, 'last'); // Execute batch operation if (actions.length > 0) { const batchResult = await batchActions(this.graph, { action: 'batch-actions', actions }); if (!batchResult) { throw new Error('Failed to create blocks'); } } } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to add content to page: ${error instanceof Error ? error.message : String(error)}` ); } } // Add a link to the created page on today's daily page try { const today = new Date(); const day = today.getDate(); const month = today.toLocaleString('en-US', { month: 'long' }); const year = today.getFullYear(); const formattedTodayTitle = `${month} ${day}${getOrdinalSuffix(day)}, ${year}`; const dailyPageQuery = `[:find ?uid . :where [?e :node/title "${formattedTodayTitle}"] [?e :block/uid ?uid]]`; const dailyPageResult = await q(this.graph, dailyPageQuery, []); const dailyPageUid = dailyPageResult ? String(dailyPageResult) : null; if (dailyPageUid) { await createBlock(this.graph, { action: 'create-block', block: { string: `Created page: [[${pageTitle}]]` }, location: { 'parent-uid': dailyPageUid, order: 'last' } }); } else { console.warn(`Could not find daily page with title: ${formattedTodayTitle}. Link to created page not added.`); } } catch (error) { console.error(`Failed to add link to daily page: ${error instanceof Error ? error.message : String(error)}`); } return { success: true, uid: pageUid }; }
- src/server/roam-server.ts:145-157 (registration)Registers the 'roam_create_page' tool in the MCP server switch statement, extracting arguments and calling ToolHandlers.createPagecase BASE_TOOL_NAMES.CREATE_PAGE: { const { title, content } = request.params.arguments as { title: string; content?: Array<{ text: string; level: number; }>; }; const result = await this.toolHandlers.createPage(title, content); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; }
- src/tools/schemas.ts:80-119 (schema)Defines the tool schema including name 'roam_create_page', description, and inputSchema for title and optional content array of blocks.[toolName(BASE_TOOL_NAMES.CREATE_PAGE)]: { name: toolName(BASE_TOOL_NAMES.CREATE_PAGE), description: 'Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the new page', }, content: { type: 'array', description: 'Initial content for the page as an array of blocks with explicit nesting levels. Note: While empty blocks (e.g., {"text": "", "level": 1}) can be used for visual spacing, they create empty entities in the database. Please use them sparingly and only for structural purposes, not for simple visual separation.', items: { type: 'object', properties: { text: { type: 'string', description: 'Content of the block' }, level: { type: 'integer', description: 'Indentation level (1-10, where 1 is top level)', minimum: 1, maximum: 10 }, heading: { type: 'integer', description: 'Optional: Heading formatting for this block (1-3)', minimum: 1, maximum: 3 } }, required: ['text', 'level'] } }, }, required: ['title'], }, },
- src/tools/tool-handlers.ts:42-44 (handler)ToolHandlers wrapper method that delegates to PageOperations.createPageasync createPage(title: string, content?: Array<{ text: string; level: number; heading?: number }>) { return this.pageOps.createPage(title, content); }
- src/types.d.ts:35-42 (schema)TypeScript interface definition for RoamCreatePage used in SDK calls.interface RoamCreatePage { action?: 'create-page'; page: { title: string; uid?: string; 'children-view-type'?: string; }; }