Your First Pack
Your First Pack¶
In this tutorial, you'll create a fully functional Intelligence Pack in about 10 minutes. By the end, you'll have a pack with multiple commands that you can run locally.
What We're Building¶
A simple "notes" pack with three commands:
- save-note – Save a note with a title
- get-note – Retrieve a note by title
- list-notes – List all saved notes
Step 1: Create the Pack¶
Use the CLI to scaffold a new pack:
huitzo pack new notes-pack
cd notes-pack
This creates the following structure:
notes-pack/
├── huitzo.yaml # Single source of truth for pack config
├── pyproject.toml # Auto-generated from huitzo.yaml
├── README.md
├── src/
│ └── notes_pack/
│ ├── __init__.py
│ └── commands/
│ ├── __init__.py
│ └── example.py
└── tests/
├── __init__.py
└── test_example.py
Step 2: Define Your Commands¶
Replace the contents of src/notes_pack/commands/example.py:
Note: The code below is an illustrative example showing the command pattern. It demonstrates the structure and SDK usage — adapt it to your own pack's domain.
# pseudocode — illustrative command pattern (adapt to your domain)
"""Notes pack commands."""
from huitzo_sdk import command, Context
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class SaveNoteArgs(BaseModel):
"""Arguments for saving a note."""
title: str = Field(description="Note title (used as identifier)")
content: str = Field(description="Note content")
class GetNoteArgs(BaseModel):
"""Arguments for retrieving a note."""
title: str = Field(description="Title of the note to retrieve")
class ListNotesArgs(BaseModel):
"""Arguments for listing notes."""
limit: int = Field(default=10, ge=1, le=100, description="Max notes to return")
@command("save-note", namespace="notes")
async def save_note(args: SaveNoteArgs, ctx: Context) -> dict:
"""Save a new note or update an existing one.
Notes are stored per-user and persist across sessions.
"""
note_data = {
"title": args.title,
"content": args.content,
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
}
# Check if note exists (for updated_at logic)
existing = await ctx.storage.get(f"note:{args.title}")
if existing:
note_data["created_at"] = existing["created_at"]
# Save the note
await ctx.storage.save(f"note:{args.title}", note_data)
ctx.log.info(f"Saved note: {args.title}")
return {
"status": "saved",
"title": args.title,
"is_update": existing is not None
}
@command("get-note", namespace="notes")
async def get_note(args: GetNoteArgs, ctx: Context) -> dict:
"""Retrieve a note by its title.
Returns the note content and metadata.
"""
note = await ctx.storage.get(f"note:{args.title}")
if note is None:
return {
"found": False,
"title": args.title,
"error": "Note not found"
}
return {
"found": True,
**note
}
@command("list-notes", namespace="notes")
async def list_notes(args: ListNotesArgs, ctx: Context) -> dict:
"""List all saved notes.
Returns note titles and creation dates.
"""
# Get all keys with the "note:" prefix
keys = await ctx.storage.list(prefix="note:")
notes = []
for key in keys[:args.limit]:
note = await ctx.storage.get(key)
if note:
notes.append({
"title": note["title"],
"created_at": note["created_at"],
"preview": note["content"][:50] + "..." if len(note["content"]) > 50 else note["content"]
})
return {
"count": len(notes),
"notes": notes
}
Step 3: Update huitzo.yaml¶
Update huitzo.yaml to register your commands. This is the single source of truth for all pack configuration — pyproject.toml is auto-generated from it.
# pseudocode — huitzo.yaml configuration
pack:
name: notes-pack
namespace: notes
version: 1.0.0
description: "Save, retrieve, and list personal notes"
visibility: private
commands:
- name: save-note
description: "Save a new note"
entry_point: "notes_pack.commands.example:save_note"
- name: get-note
description: "Retrieve a note by title"
entry_point: "notes_pack.commands.example:get_note"
- name: list-notes
description: "List all saved notes"
entry_point: "notes_pack.commands.example:list_notes"
Note: You never need to edit
pyproject.tomldirectly. It is regenerated automatically when you runhuitzo pack devorhuitzo pack build. You can also runhuitzo pack syncto regenerate it manually.
Step 4: Start Development Session¶
Start a development session to test your pack:
huitzo pack dev
This starts a local proxy that connects to Huitzo's cloud sandbox where your pack code executes. You should see:
🔐 Authenticating... OK ([email protected])
📦 Packaging pack... 3 commands found
☁️ Uploading to sandbox... done
🚀 Development session started
Proxy: http://localhost:8080
API docs: http://localhost:8080/docs
Commands:
- POST /api/v1/commands/notes/save-note
- POST /api/v1/commands/notes/get-note
- POST /api/v1/commands/notes/list-notes
📊 Quota: 847/1000 executions this month
⌨️ Press Ctrl+C to stop
[12:34:56] Watching for file changes...
How it works: The proxy runs on your machine (
localhost:8080), but your pack code executes in Huitzo's cloud sandbox. This means you don't need PostgreSQL, Redis, or any infrastructure locally—the sandbox handles everything.
Step 5: Test Your Commands¶
Using curl¶
# Save a note
curl -X POST http://localhost:8080/api/v1/commands/notes/save-note \
-H "Content-Type: application/json" \
-d '{"title": "My First Note", "content": "Hello, Huitzo!"}'
# Response:
# {"status": "saved", "title": "My First Note", "is_update": false}
# Get the note
curl -X POST http://localhost:8080/api/v1/commands/notes/get-note \
-H "Content-Type: application/json" \
-d '{"title": "My First Note"}'
# Response:
# {"found": true, "title": "My First Note", "content": "Hello, Huitzo!", ...}
# List all notes
curl -X POST http://localhost:8080/api/v1/commands/notes/list-notes \
-H "Content-Type: application/json" \
-d '{}'
# Response:
# {"count": 1, "notes": [{"title": "My First Note", ...}]}
Using the API Docs¶
Open http://localhost:8080/docs in your browser to see the interactive API documentation for your pack's commands.
Huitzo Documentation¶
Your dev session also starts a local documentation server with the full Huitzo docs:
- Web UI: http://localhost:8124
- MCP endpoint: http://localhost:8124/mcp (for AI coding assistants)
This gives you instant access to SDK reference, examples, and guides while developing.
Step 6: Write Tests¶
Replace tests/test_example.py:
"""Tests for notes pack."""
import pytest
from notes_pack.commands.example import SaveNoteArgs, GetNoteArgs, ListNotesArgs
class TestSaveNoteArgs:
"""Test SaveNoteArgs validation."""
def test_valid_args(self):
args = SaveNoteArgs(title="Test", content="Content")
assert args.title == "Test"
assert args.content == "Content"
def test_empty_title_fails(self):
with pytest.raises(ValueError):
SaveNoteArgs(title="", content="Content")
class TestGetNoteArgs:
"""Test GetNoteArgs validation."""
def test_valid_args(self):
args = GetNoteArgs(title="Test")
assert args.title == "Test"
class TestListNotesArgs:
"""Test ListNotesArgs validation."""
def test_default_limit(self):
args = ListNotesArgs()
assert args.limit == 10
def test_custom_limit(self):
args = ListNotesArgs(limit=50)
assert args.limit == 50
def test_limit_bounds(self):
with pytest.raises(ValueError):
ListNotesArgs(limit=0)
with pytest.raises(ValueError):
ListNotesArgs(limit=101)
Run tests:
huitzo pack test
# Or: pytest tests/
Step 7: Validate Your Pack¶
Before publishing, validate your pack:
huitzo pack validate
# Output:
# ✅ Manifest valid
# ✅ 3 commands found
# ✅ All commands have valid schemas
# ✅ Tests passing
# ✅ Ready to build!
Step 8: Build and Publish¶
# Build the distribution
huitzo pack build
# Publish to your Huitzo instance (publishes under your organization scope)
huitzo pack publish
After publishing, your commands will be available under your organization's scoped namespace:
@yourorg/notes-pack/save-note
@yourorg/notes-pack/get-note
@yourorg/notes-pack/list-notes
See Namespaces for more on scoped namespace resolution and WebCLI navigation.
What You've Learned¶
In this tutorial, you:
- ✅ Scaffolded a new pack with
huitzo pack new - ✅ Created commands with Pydantic validation
- ✅ Used
ctx.storagefor data persistence - ✅ Used
ctx.logfor structured logging - ✅ Started a cloud-connected development session
- ✅ Tested commands via the local proxy
- ✅ Wrote unit tests
- ✅ Validated and built your pack
Next Steps¶
Add More Features¶
Try enhancing your notes pack:
- Add a
delete-notecommand - Add search functionality with
search-notes - Add tags to notes for categorization
- Send email notifications when notes are created
Learn More¶
- SDK Overview – Understand the full SDK
- Namespaces – Scoped namespaces and navigation
- Context Reference – All available services
- Storage Reference – Advanced storage patterns
- Pack Manifest – Configure your pack
Deploy¶
- Self-Hosting – Run your own Huitzo instance
- Configuration – Configure your environment
Complete Example¶
For reference, here's the full command pattern with all four commands:
# pseudocode — complete example showing all four commands together
"""Notes pack commands."""
from huitzo_sdk import command, Context
from pydantic import BaseModel, Field
from datetime import datetime
class SaveNoteArgs(BaseModel):
title: str = Field(description="Note title")
content: str = Field(description="Note content")
class GetNoteArgs(BaseModel):
title: str = Field(description="Note title")
class DeleteNoteArgs(BaseModel):
title: str = Field(description="Note title")
class ListNotesArgs(BaseModel):
limit: int = Field(default=10, ge=1, le=100)
@command("save-note", namespace="notes")
async def save_note(args: SaveNoteArgs, ctx: Context) -> dict:
"""Save a note."""
existing = await ctx.storage.get(f"note:{args.title}")
note_data = {
"title": args.title,
"content": args.content,
"created_at": existing["created_at"] if existing else datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
}
await ctx.storage.save(f"note:{args.title}", note_data)
ctx.log.info(f"Saved note: {args.title}")
return {"status": "saved", "title": args.title, "is_update": existing is not None}
@command("get-note", namespace="notes")
async def get_note(args: GetNoteArgs, ctx: Context) -> dict:
"""Get a note by title."""
note = await ctx.storage.get(f"note:{args.title}")
if note is None:
return {"found": False, "title": args.title, "error": "Not found"}
return {"found": True, **note}
@command("delete-note", namespace="notes")
async def delete_note(args: DeleteNoteArgs, ctx: Context) -> dict:
"""Delete a note."""
exists = await ctx.storage.exists(f"note:{args.title}")
if not exists:
return {"deleted": False, "error": "Not found"}
await ctx.storage.delete(f"note:{args.title}")
ctx.log.info(f"Deleted note: {args.title}")
return {"deleted": True, "title": args.title}
@command("list-notes", namespace="notes")
async def list_notes(args: ListNotesArgs, ctx: Context) -> dict:
"""List all notes."""
keys = await ctx.storage.list(prefix="note:")
notes = []
for key in keys[:args.limit]:
note = await ctx.storage.get(key)
if note:
notes.append({
"title": note["title"],
"created_at": note["created_at"],
"preview": note["content"][:50] + ("..." if len(note["content"]) > 50 else "")
})
return {"count": len(notes), "notes": notes}