Skip to main content
Glama

workflowy_update_node

Modify existing WorkFlowy nodes by updating their name, note content, layout style, or completion status using the node's unique identifier.

Instructions

Update an existing WorkFlowy node

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
node_idYes
nameNo
noteNo
layout_modeNo
_completedNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
chNoChild nodes
cpNoCompletion status (for tests)
idYesUnique identifier for the node
dataNoNode data including layoutMode
nameNoText content of the node
noteNoNote content attached to the node
parentIdNoParent node ID
priorityNoSort order
createdAtNoCreation timestamp (Unix timestamp)
modifiedAtNoLast modification timestamp
completedAtNoCompletion timestamp (null if not completed)

Implementation Reference

  • MCP tool handler and registration for workflowy_update_node. Wraps client.update_node with rate limiting.
    @mcp.tool(name="workflowy_update_node", description="Update an existing WorkFlowy node")
    async def update_node(
        node_id: str,
        name: str | None = None,
        note: str | None = None,
        layout_mode: Literal["bullets", "todo", "h1", "h2", "h3"] | None = None,
        _completed: bool | None = None,
    ) -> WorkFlowyNode:
        """Update an existing WorkFlowy node.
    
        Args:
            node_id: The ID of the node to update
            name: New text content for the node (optional)
            note: New note/description (optional)
            layout_mode: New layout mode for the node (bullets, todo, h1, h2, h3) (optional)
            _completed: New completion status (not used - use complete_node/uncomplete_node)
    
        Returns:
            The updated WorkFlowy node
        """
        client = get_client()
    
        request = NodeUpdateRequest(  # type: ignore[call-arg]
            name=name,
            note=note,
            layoutMode=layout_mode,
        )
    
        if _rate_limiter:
            await _rate_limiter.acquire()
    
        try:
            node = await client.update_node(node_id, request)
            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
  • Pydantic schema NodeUpdateRequest defining input parameters for node updates.
    class NodeUpdateRequest(BaseModel):
        """Request payload for updating an existing node."""
    
        name: str | None = Field(None, description="New text content")
        note: str | None = Field(None, description="New note content")
        layoutMode: Literal["bullets", "todo", "h1", "h2", "h3"] | None = Field(
            None, description="New display mode (bullets, todo, h1, h2, h3)"
        )
    
        def has_updates(self) -> bool:
            """Check if at least one field is provided for update."""
            return any(getattr(self, field) is not None for field in self.model_fields)
  • Core API client implementation of update_node: HTTP POST, validation, escaping, retries, error handling.
    async def update_node(self, node_id: str, request: NodeUpdateRequest, max_retries: int = 10) -> WorkFlowyNode:
        """Update an existing node with exponential backoff retry.
        
        Args:
            node_id: The ID of the node to update
            request: Node update request
            max_retries: Maximum retry attempts (default 10)
        """
        import asyncio
    
        logger = _ClientLogger()
        
        # Validate and escape name field if being updated
        if request.name is not None:
            processed_name, name_warning = self._validate_name_field(request.name)
            if processed_name is not None:
                request.name = processed_name
            if name_warning:
                logger.info(name_warning)
        
        # Validate and escape note field if being updated
        if request.note is not None:
            # Note: update_node doesn't have _internal_call flag yet, always validates
            processed_note, message = self._validate_note_field(request.note)
            
            if processed_note is None and message:  # Blocking error
                raise NetworkError(message)
            
            # Strip override token if present
            if processed_note and processed_note.startswith("<<<LITERAL_BACKSLASH_N_INTENTIONAL>>>"):
                processed_note = processed_note.replace("<<<LITERAL_BACKSLASH_N_INTENTIONAL>>>", "", 1)
            
            # Use processed (escaped) note
            request.note = processed_note
            
            # Log warning if escaping occurred
            if message and "AUTO-ESCAPED" in message:
                logger.info(message)
        
        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.post(
                    f"/nodes/{node_id}", json=request.model_dump(exclude_none=True)
                )
                data = await self._handle_response(response)
                # API returns {"status": "ok"} - fetch updated node
                if isinstance(data, dict) and data.get('status') == 'ok':
                    get_response = await self.client.get(f"/nodes/{node_id}")
                    node_data = await self._handle_response(get_response)
                    node = WorkFlowyNode(**node_data["node"])
                else:
                    # Fallback for unexpected format
                    node = WorkFlowyNode(**data)
    
                # Best-effort: mark this node as dirty so that subsequent
                # /nodes-export-based operations touching this subtree can
                # trigger a refresh when needed.
                try:
                    self._mark_nodes_export_dirty([node_id])
                except Exception:
                    # Cache dirty marking must never affect API behavior
                    pass
    
                return node
                    
            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 update_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 update_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("update_node") from err
        
        raise NetworkError("update_node failed after maximum retries")
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. While 'Update' implies mutation, it doesn't disclose whether this requires specific permissions, whether changes are reversible, what happens when only some fields are provided (partial updates), or what the response contains. For a mutation tool with 5 parameters and no annotation coverage, this is a significant gap in behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise at just 5 words, with no wasted language. It's front-loaded with the core action and target. While this conciseness comes at the cost of completeness, the structure itself is efficient with every word serving a purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given this is a mutation tool with 5 parameters, 0% schema description coverage, no annotations, but with an output schema, the description is incomplete. While the output schema may document return values, the description fails to provide essential context about what can be updated, how updates work, when to use this versus sibling tools, or behavioral implications. For a tool with this complexity, the description should do significantly more.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, meaning none of the 5 parameters have descriptions in the schema. The tool description provides zero information about what the parameters mean, their purposes, or how they interact. It doesn't even mention that parameters exist beyond node_id, leaving the agent to guess what can be updated based on parameter names alone.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Update') and target ('an existing WorkFlowy node'), providing specific verb+resource. However, it doesn't distinguish this tool from sibling update tools like 'workflowy_complete_node' or 'workflowy_uncomplete_node' which also modify nodes, nor does it mention what aspects can be updated (name, note, layout_mode, completion status).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention when to prefer this over 'workflowy_complete_node' for marking completion, or 'workflowy_move_node' for structural changes, nor does it specify prerequisites like needing an existing node_id. There's no explicit when/when-not context provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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