Skip to main content
Glama

workflowy_create_single_node

Create a single node in WorkFlowy with a name, optional parent ID, and optional note for hierarchical task management.

Instructions

DEPRECATED: Use workflowy_etch (ETCH) instead

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nameYes
parent_idNo
noteNo

Implementation Reference

  • Registration of deprecated 'workflowy_create_single_node' tool. Raises ValueError directing to ETCH or warning version.
    # Tool: Create Single Node (Base - Deprecated)
    @mcp.tool(name="workflowy_create_single_node", description="DEPRECATED: Use workflowy_etch (ETCH) instead")
    async def create_single_node_base(
        name: str,
        parent_id: str | None = None,
        note: str | None = None,
    ) -> dict:
        """Deprecated - use ETCH instead."""
        raise ValueError("""⚠️ FUNCTION RENAMED
    
    The function 'workflowy_create_single_node' has been renamed to 'workflowy_create_single_node__WARNING__prefer_ETCH'.
    
    BUT MORE IMPORTANTLY: Use workflowy_etch (ETCH command) instead!
    
    ✅ RECOMMENDED:
      workflowy_etch(
        parent_id="...",
        nodes=[{"name": "Your node", "note": "...", "children": []}]
      )
    
    ETCH is better:
    - Works for 1 node or 100 nodes
    - Validation and auto-escaping built-in
    - Same performance, more capability
    
    📚 Build the ETCH habit!
    """)
  • MCP tool handler for single node creation (warning version). Validates secret_code, prepares NodeCreateRequest, calls core client.create_node with retry/rate-limit handling.
    # Tool: Create Single Node (With Warning)
    @mcp.tool(name="workflowy_create_single_node__WARNING__prefer_ETCH", description="⚠️ WARNING: Prefer workflowy_etch (ETCH) instead. This creates ONE node only.")
    async def create_node(
        name: str,
        parent_id: str | None = None,
        note: str | None = None,
        layout_mode: Literal["bullets", "todo", "h1", "h2", "h3"] | None = None,
        position: Literal["top", "bottom"] = "bottom",
        _completed: bool = False,
        secret_code: str | None = None,
    ) -> dict:
        """Create a SINGLE node in WorkFlowy.
        
        ⚠️ WARNING: Prefer workflowy_etch (ETCH) for creating 2+ nodes.
        
        This tool is ONLY for:
        - Adding one VYRTHEX to existing log (real-time work)
        - One quick update to a known node
        - Live work in progress
    
        Args:
            name: The text content of the node
            parent_id: ID of the parent node (optional)
            note: Additional note/description for the node
            layout_mode: Layout mode for the node (bullets, todo, h1, h2, h3) (optional)
            position: Where to place the new node - "bottom" (default) or "top"
            _completed: Whether the node should be marked as completed (not used)
            secret_code: Authorization code from Dan (required for WARNING functions)
    
        Returns:
            Dictionary with node data and warning message
        """
        # 🔐 SECRET CODE VALIDATION
        is_valid, error = validate_secret_code(secret_code, "workflowy_create_single_node__WARNING__prefer_ETCH")
        if not is_valid:
            raise ValueError(error)
        
        client = get_client()
    
        request = NodeCreateRequest(  # type: ignore[call-arg]
            name=name,
            parent_id=parent_id,
            note=note,
            layoutMode=layout_mode,
            position=position,
        )
    
        if _rate_limiter:
            await _rate_limiter.acquire()
    
        try:
            node = await client.create_node(request)
            if _rate_limiter:
                _rate_limiter.on_success()
            
            # Return node data with warning message
            return {
                **node.model_dump(),
                "_warning": "⚠️ WARNING: You just created a SINGLE node. For 2+ nodes, use workflowy_etch instead (same performance, more capability)."
            }
        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
    
    
    # Tool: Update Node
  • Core implementation of node creation: validates/escapes fields, handles retries/rate-limits, posts to /nodes/, fetches created node.
        async def create_node(
            self, request: NodeCreateRequest, _internal_call: bool = False, max_retries: int = 10
        ) -> WorkFlowyNode:
            """Create a new node in WorkFlowy with exponential backoff retry.
            
            Args:
                request: Node creation request
                _internal_call: Internal flag - bypasses single-node forcing function (not exposed to MCP)
                max_retries: Maximum retry attempts (default 10)
            """
            import asyncio
    
            logger = _ClientLogger()
    
            # Check for single-node override token (skip if internal call)
            if not _internal_call:
                SINGLE_NODE_TOKEN = "<<<I_REALLY_NEED_SINGLE_NODE>>>"
                
                if request.name and request.name.startswith(SINGLE_NODE_TOKEN):
                    # Strip token and proceed
                    request.name = request.name.replace(SINGLE_NODE_TOKEN, "", 1)
                else:
                    # Suggest ETCH instead
                    raise NetworkError("""⚠️ PREFER ETCH - Use workflowy_etch for consistency and capability
    
    You called workflowy_create_single_node, but workflowy_etch has identical performance.
    
    ✅ RECOMMENDED (same speed, more capability):
      workflowy_etch(
        parent_id="...",
        nodes=[{"name": "Your node", "note": "...", "children": []}]
      )
    
    📚 Benefits of ETCH:
      - Same 1 tool call (no performance difference)
      - Validation and auto-escaping built-in
      - Works for 1 node or 100 nodes (consistent pattern)
      - Trains you to think in tree structures
    
    ⚙️ OVERRIDE (if you truly need single-node operation):
      workflowy_create_single_node(
        name="<<<I_REALLY_NEED_SINGLE_NODE>>>Your node",
        ...
      )
    
    🎯 Build the ETCH habit - it's your go-to tool!
    """)
            
            # Validate and escape name field
            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
            # Skip newline check if internal call (for bulk operations testing)
            processed_note, note_warning = self._validate_note_field(request.note, skip_newline_check=_internal_call)
            
            if processed_note is None and note_warning:  # Blocking error
                raise NetworkError(note_warning)
            
            # 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 note_warning and "AUTO-ESCAPED" in note_warning:
                logger.info(note_warning)
    
            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("/nodes/", json=request.model_dump(exclude_none=True))
                    data = await self._handle_response(response)
                    # Create endpoint returns just {"item_id": "..."}
                    item_id = data.get("item_id")
                    if not item_id:
                        raise NetworkError(f"Invalid response from create endpoint: {data}")
    
                    # Fetch the created node to get actual saved state (including note field)
                    get_response = await self.client.get(f"/nodes/{item_id}")
                    node_data = await self._handle_response(get_response)
                    node = WorkFlowyNode(**node_data["node"])
    
                    # Best-effort: mark this node as dirty in the /nodes-export cache so that
                    # any subtree exports including it 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 create_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
                    _log(
                        f"Network error on create_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("create_node") from err
    
            raise NetworkError("create_node failed after maximum retries")
  • Pydantic schema NodeCreateRequest defining input validation for create_node requests.
    class NodeCreateRequest(BaseModel):
        """Request payload for creating a new node."""
    
        parent_id: str | None = Field(None, description="Parent node ID ('None' for root level)")
        name: str = Field(..., description="Text content (required)")
        note: str | None = Field(None, description="Note content (optional)")
        layoutMode: Literal["bullets", "todo", "h1", "h2", "h3"] | None = Field(
            None, description="Display mode (bullets, todo, h1, h2, h3)"
        )
        position: Literal["top", "bottom"] | None = Field(
            "top", description="Position: 'top' or 'bottom' (default: 'top')"
        )
    
        @field_validator("name")
        @classmethod
        def validate_name(cls, v: str) -> str:
            """Ensure name is non-empty."""
            if not v or not v.strip():
                raise ValueError("Node name must be non-empty")
            return v
    
        @field_validator("parent_id")
        @classmethod
        def validate_parent_id(cls, v: str | None) -> str | None:
            """Keep parent_id as-is - None means root level."""
            return v
  • Helper for validating and escaping name field (double-encode for GUI, preserve whitelisted markup). Used in create_node.
    def _validate_name_field(name: str | None) -> tuple[str | None, str | None]:
        """Validate and smart-escape name field for Workflowy compatibility.
        
        Workflowy NAME field behavior (Dec 2025):
        - API decodes entities ONCE on input before storage
        - GUI decodes entities AGAIN when rendering
        - Result: Must DOUBLE-ENCODE for proper display
        
        New behavior with parser:
        - Properly matched whitelisted tags (<b>/<i>/<s>/<code> and allowed
          <span class="colored c-...">) are preserved as markup ranges
        - EVERYTHING ELSE is treated as plain text and has < and > escaped
        - Then we double-encode '&' across the entire string so that a
          round-trip API+GUI decode yields the intended characters
        """
        if name is None:
            return (None, None)
    
        # Per-node whitener override via inline token in the NAME text
        mode, clean_name = WorkFlowyClientCore._extract_whitener_mode(name)
        if mode == "raw":
            return (clean_name, None)
        if clean_name is not None:
            name = clean_name
    
        text = name
        segments = WorkFlowyClientCore._segment_whitelisted_markup(text)
    
        import re
        wrapped_segments = []
        for seg in segments:
            if seg["kind"] == "markup":
                v = seg["value"]
                if re.fullmatch(r'<span\s+class="colored\s+(?:c|bc)-[^"]+">.*?</span>', v):
                    seg = {"kind": "markup", "value": f"<b>{v}</b>"}
            wrapped_segments.append(seg)
        segments = wrapped_segments
    
        # SINGLE-STAGE: escape &, <, > in text segments only; leave markup untouched
        result_chars: list[str] = []
        for seg in segments:
            if seg["kind"] == "markup":
                result_chars.append(seg["value"])
            else:
                for ch in seg["value"]:
                    if ch == '&':
                        result_chars.append('&')
                    elif ch == '<':
                        result_chars.append('<')
                    elif ch == '>':
                        result_chars.append('>')
                    else:
                        result_chars.append(ch)
    
        escaped_name = ''.join(result_chars)
        return (escaped_name, None)

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