Recipe — create a note
Create a note from chat in Webbee — a runnable extension recipe where the classifier generates the full note body itself and writes it into the tool args.
A note-creation tool. Demonstrates the content-field rule: when the user asks for content (an essay, a summary, a draft), the classifier itself generates the full content into args — there is no downstream "drafting LLM" that fills empty fields.
from pydantic import BaseModel, Field
class CreateNoteParams(BaseModel):
title: str = Field(
description="Short title for the note (1 sentence max).",
)
content_text: str = Field(
description=(
"The FULL note content. Write the actual content here. "
"If the user asked for a 200-word essay, write all 200 words. "
"NEVER write placeholders like '<essay 200 words>' — there is no downstream agent that will fill them."
),
)
folder_id: str | None = Field(
None,
description="Optional UUID of the folder to put the note in. Obtain from list_folders — never invent.",
)
tags: list[str] = Field(default_factory=list, description="Optional tags.")from imperal_sdk import ChatExtension, ActionResult
from main import ext # the Extension declared in main.py
from .schemas import CreateNoteParams
# A ChatExtension instance is what provides the @chat.function decorator.
chat = ChatExtension(ext, "notes_mini", description="Create and manage notes in plain language.")
@chat.function(
"create_note",
description="Create a new note with the user's content. The content_text MUST be the actual content — never a placeholder.",
action_type="write",
effects=["note.create"],
)
async def create_note(ctx, params: CreateNoteParams) -> ActionResult:
note = await ctx.http.post("/notes", json=params.model_dump(exclude_none=True))
return ActionResult.success(
summary=f"Created note: {note['title']}",
data={"note_id": note["id"], "url": note.get("panel_url")},
)from imperal_sdk import Extension
from . import handlers_chat # noqa: F401
ext = Extension(
"notes-mini",
display_name="Notes Mini",
description="Create and manage notes in plain language.",
icon="file-text",
actions_explicit=True,
)Try it in chat
"create a note titled 'Q3 strategy' — write a 150-word summary of why we're shifting focus to enterprise"
The classifier:
- Picks
create_note. - Fills
title="Q3 strategy". - Generates the actual 150-word summary into
content_text.
Your handler stores it. Done.
Why this works
When you ask for content, Webbee writes the full content directly into the tool arguments and then runs your handler with those arguments — there is no separate downstream agent that fills empty fields. Your schema field description is what tells the agent to produce real content rather than a placeholder, so it is the authoritative pattern: spell out exactly what belongs in each field.
Variations
class AppendToNoteParams(BaseModel):
note_id: str = Field(description="UUID of the note. Obtain from list_notes — never invent.")
content_to_append: str = Field(description="The content to append. Markdown supported.")
@chat.function(
"append_to_note",
description="Append content to an existing note.",
action_type="write",
id_projection="note_id",
)
async def append_to_note(ctx, params) -> ActionResult: ...class CreateNoteParams(BaseModel):
title: str = Field(description="Note title.")
content_markdown: str = Field(description="Note body in Markdown (use ## for headings, ** for bold, * for italics).")The LLM writes Markdown when the description names it. Your storage / panel rendering handles MD → HTML.
class CreateNoteParams(BaseModel):
title: str = Field(description="Note title.")
content_text: str = Field(description="The full note content.")
folder_id: str | None = Field(
None,
description="UUID of folder. Obtain from list_folders or resolve_folder. Never invent.",
)For chains where the user says "create a note in the work folder", the classifier first calls resolve_folder(name="work"), gets the folder UUID, then calls create_note(folder_id=<uuid>, ...).
What this avoids
Empty content fields
If your description says 'optional content' the LLM may leave it empty expecting downstream filling. Always demand explicit content.
Placeholder leaks
Without the 'NEVER write placeholders' clause, BYOLLMs sometimes emit '<200-word essay>' literal strings. Tightening the description fixed it federally.
Hallucinated folder IDs
The platform rejects fabricated or placeholder IDs before your handler runs, but the durable fix is description-side: tell the model where to get the real ID (e.g. 'Obtain from list_folders — never invent').
Where to next
Recipe — send an email
Send an email from a Webbee extension — a complete, copy-paste send_email tool with a Pydantic schema, an async handler, and tests, federal-clean and runnable.
Recipe — multistep from one message
Drive two tools from one chat message in Webbee. Its brain runs an iterative tool-use loop — call a tool, read the real result, decide the next step.