Skip to main content
Glama

workflowy_get_node

Retrieve specific nodes from WorkFlowy outlines to access hierarchical task and note data for integration with LLM applications.

Instructions

DEPRECATED: Use workflowy_glimpse (GLIMPSE) instead

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
node_idYes

Implementation Reference

  • MCP tool registration for 'workflowy_get_node' (deprecated handler that raises ValueError directing to use workflowy_glimpse)
    @mcp.tool(name="workflowy_get_node", description="DEPRECATED: Use workflowy_glimpse (GLIMPSE) instead") async def get_node_base(node_id: str) -> dict: """Deprecated - use GLIMPSE instead.""" raise ValueError("""⚠️ FUNCTION RENAMED The function 'workflowy_get_node' has been renamed to 'workflowy_get_node__WARNING__prefer_glimpse'. BUT MORE IMPORTANTLY: Use workflowy_glimpse (GLIMPSE command) instead! ✅ RECOMMENDED: workflowy_glimpse(node_id="...") Returns: {"root": {...}, "children": [...]} with complete tree structure. GLIMPSE is better: - Gets root node metadata (name, note) - Gets full children tree (not just direct children) - One call gets everything 📚 Build the GLIMPSE habit! """)
  • Functional MCP handler (under warning name) that validates secret_code and calls client.get_node(node_id)
    @mcp.tool(name="workflowy_get_node__WARNING__prefer_glimpse", description="⚠️ WARNING: Prefer workflowy_glimpse (GLIMPSE) for reading trees. Retrieve a specific WorkFlowy node by ID") async def get_node( node_id: str, secret_code: str | None = None, ) -> WorkFlowyNode: """Retrieve a specific WorkFlowy node. Args: node_id: The ID of the node to retrieve secret_code: Authorization code from Dan (required for WARNING functions) Returns: The requested WorkFlowy node """ # 🔐 SECRET CODE VALIDATION is_valid, error = validate_secret_code(secret_code, "workflowy_get_node__WARNING__prefer_glimpse") if not is_valid: raise ValueError(error) client = get_client() if _rate_limiter: await _rate_limiter.acquire() try: node = await client.get_node(node_id) if _rate_limiter: _rate_limiter.on_success() return node except Exception as e: if _rate_limiter and hasattr(e, "__class__") and e.__class__.__name__ == "RateLimitError": _rate_limiter.on_rate_limit(getattr(e, "retry_after", None)) raise
  • Core implementation of get_node: performs HTTP GET /nodes/{node_id}, handles retries, rate limits, errors, and parses into WorkFlowyNode
    async def get_node(self, node_id: str, max_retries: int = 10) -> WorkFlowyNode: """Retrieve a specific node by ID with exponential backoff retry. Args: node_id: The ID of the node to retrieve max_retries: Maximum retry attempts (default 10) """ import asyncio logger = _ClientLogger() retry_count = 0 base_delay = 1.0 while retry_count < max_retries: # Force delay at START of each iteration (rate limit protection) await asyncio.sleep(API_RATE_LIMIT_DELAY) try: response = await self.client.get(f"/nodes/{node_id}") data = await self._handle_response(response) # API returns {"node": {...}} structure if isinstance(data, dict) and "node" in data: return WorkFlowyNode(**data["node"]) else: # Fallback for unexpected format return WorkFlowyNode(**data) except RateLimitError as e: retry_count += 1 retry_after = getattr(e, 'retry_after', None) or (base_delay * (2 ** retry_count)) logger.warning( f"Rate limited on get_node. Retry after {retry_after}s. " f"Attempt {retry_count}/{max_retries}" ) if retry_count < max_retries: await asyncio.sleep(retry_after) else: raise except NetworkError as e: retry_count += 1 logger.warning( f"Network error on get_node: {e}. Retry {retry_count}/{max_retries}" ) if retry_count < max_retries: await asyncio.sleep(base_delay * (2 ** retry_count)) else: raise except httpx.TimeoutException as err: retry_count += 1 logger.warning( f"Timeout error: {err}. Retry {retry_count}/{max_retries}" ) if retry_count < max_retries: await asyncio.sleep(base_delay * (2 ** retry_count)) else: raise TimeoutError("get_node") from err raise NetworkError("get_node failed after maximum retries")
  • Pydantic model defining the output schema WorkFlowyNode for get_node response
    class WorkFlowyNode(BaseModel): """Represents a single node in the WorkFlowy outline hierarchy.""" # API fields (what the API actually returns) id: str = Field(..., description="Unique identifier for the node") name: str | None = Field( None, validation_alias=AliasChoices("name", "nm"), description="Text content of the node" ) note: str | None = Field( None, validation_alias=AliasChoices("note", "no"), description="Note content attached to the node", ) priority: int | None = Field(None, description="Sort order") data: dict[str, Any] | None = Field(None, description="Node data including layoutMode") createdAt: int | None = Field( None, validation_alias=AliasChoices("createdAt", "created"), description="Creation timestamp (Unix timestamp)", ) modifiedAt: int | None = Field( None, validation_alias=AliasChoices("modifiedAt", "modified"), description="Last modification timestamp", ) completedAt: int | None = Field( None, description="Completion timestamp (null if not completed)" ) # Nested structure fields children: list["WorkFlowyNode"] | None = Field(None, alias="ch", description="Child nodes") parent_id: str | None = Field(None, alias="parentId", description="Parent node ID") # Handle 'cp' field for backward compatibility - we'll compute from completedAt completed_flag: bool | None = Field( None, alias="cp", description="Completion status (for tests)" ) @property def layoutMode(self) -> str | None: """Extract layoutMode from data field.""" if self.data and isinstance(self.data, dict): return self.data.get("layoutMode") return None # Backward compatibility aliases for tests @property def nm(self) -> str | None: """Backward compatibility for name field.""" return self.name @property def no(self) -> str | None: """Backward compatibility for note field.""" return self.note @property def cp(self) -> bool: """Backward compatibility for completed status.""" # Use completed_flag if it was set (from tests), otherwise check completedAt if self.completed_flag is not None: return self.completed_flag return self.completedAt is not None @property def ch(self) -> list["WorkFlowyNode"] | None: """Backward compatibility for children field.""" return self.children @property def created(self) -> int: """Backward compatibility for created timestamp.""" return self.createdAt or 0 @property def modified(self) -> int: """Backward compatibility for modified timestamp.""" return self.modifiedAt or 0 @field_validator("id") @classmethod def validate_id(cls, v: str) -> str: """Ensure ID is non-empty.""" if not v or not v.strip(): raise ValueError("Node ID must be non-empty") return v @field_validator("createdAt", "modifiedAt", "completedAt") @classmethod def validate_timestamp(cls, v: int | None) -> int | None: """Ensure timestamps are positive.""" if v is not None and v <= 0: raise ValueError("Timestamp must be positive") return v def model_dump(self, **kwargs: Any) -> dict[str, Any]: """Custom serialization to include backward compatibility fields.""" data: dict[str, Any] = super().model_dump(**kwargs) # Add backward compatibility fields for tests data["nm"] = self.name data["no"] = self.note data["cp"] = self.cp data["ch"] = self.children data["created"] = self.createdAt or 0 data["modified"] = self.modifiedAt or 0 return data class Config: """Pydantic model configuration.""" populate_by_name = True # Allow both field names and aliases json_schema_extra = { "example": { "id": "node-123", "name": "Example Node", "note": "This is a note", "priority": 1, "layoutMode": "bullets", "createdAt": 1704067200, "modifiedAt": 1704067200, "completedAt": None, "children": [], } } # Enable forward references for recursive model WorkFlowyNode.model_rebuild()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/daniel347x/workflowy-mcp-fixed'

If you have feedback or need assistance with the MCP directory API, please join our Discord server