Commands Reference

Commands Reference

Commands are the core building blocks of Intelligence Packs. This reference covers everything you need to know about creating commands.

The @command Decorator

The @command decorator is the primary way to define commands:

from huitzo_sdk import command, Context
from pydantic import BaseModel

class MyArgs(BaseModel):
    param1: str
    param2: int = 10

@command("my-command", namespace="my-pack")
async def my_command(args: MyArgs, ctx: Context) -> dict:
    """Command description shown in help."""
    return {"result": args.param1 * args.param2}

Decorator Parameters

Parameter Type Default Description
name str Required Command name (lowercase, hyphens allowed)
namespace str Required Pack namespace (typically pack-name)
version str "1.0.0" Command version (semver)
timeout int 60 Max execution time in seconds
retries int 3 Number of retry attempts
retry_backoff float 1.0 Exponential backoff base (seconds)
retry_max_wait int 60 Maximum wait between retries (seconds)
queue str "default" Task queue: "fast", "medium", "long"
output_format str "auto" Output format: "json", "text", "auto"
description str From docstring Override command description

Retry Behavior: On failure, the SDK waits retry_backoff * (2 ^ attempt) seconds before retrying, capped at retry_max_wait. See Error Handling for details.

Example: Full Configuration

@command(
    "analyze-data",
    namespace="analytics",
    version="2.1.0",
    timeout=3600,           # 1 hour max
    retries=5,              # 5 retry attempts
    queue="long",           # Long-running queue
    output_format="json",   # Always JSON output
    description="Analyze large datasets with ML models"
)
async def analyze_data(args: AnalyzeArgs, ctx: Context) -> AnalysisResult:
    ...

Timeout Hierarchy

Huitzo uses a three-tier timeout system to ensure commands complete within reasonable bounds while giving flexibility at different levels:

Command Timeout ≤ Pack Timeout ≤ Platform Global Timeout

Timeout Levels

Level Defined By Default Description
Command @command decorator 60s Max time for this specific command
Pack huitzo.yaml manifest 3600s Must be ≥ max command timeout in pack
Platform Admin configuration 28800s (8h) Hard limit set by Huitzo (SaaS) or self-hosted admin

How Timeouts Apply

  1. Command-level timeout is specified in the @command decorator
  2. Pack-level timeout in huitzo.yaml must be ≥ the maximum of all command timeouts
  3. Platform timeout is the hard ceiling - commands cannot exceed this regardless of other settings

Example Configuration

# Command timeout: 300 seconds
@command("analyze", namespace="analytics", timeout=300)
async def analyze(args: Args, ctx: Context):
    ...
# huitzo.yaml - Pack timeout must be >= 300 (the max command timeout)
pack:
  name: "analytics"
  timeout: 600  # Pack-level ceiling
# Platform timeout (environment variable for self-hosted)
HUITZO_GLOBAL_TIMEOUT=28800  # 8 hours max

Validation Rules

  • Pack build fails if any command timeout exceeds the pack timeout
  • Command rejected at runtime if pack timeout exceeds platform global timeout
  • Self-hosted admins can configure their platform global timeout via environment variables

Input Arguments (Pydantic Models)

All command inputs must be Pydantic models:

from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from datetime import date

class ReportArgs(BaseModel):
    """Arguments for report generation."""

    # Required field
    report_type: str = Field(description="Type of report to generate")

    # Optional with default
    start_date: Optional[date] = Field(
        default=None, 
        description="Start date for report"
    )

    # List field
    departments: List[str] = Field(
        default_factory=list,
        description="Filter by departments"
    )

    # Field with constraints
    max_rows: int = Field(
        default=1000,
        ge=1,
        le=100000,
        description="Maximum rows to include"
    )

    # Custom validation
    @field_validator("report_type")
    @classmethod
    def validate_report_type(cls, v: str) -> str:
        valid_types = ["sales", "inventory", "financial"]
        if v not in valid_types:
            raise ValueError(f"Must be one of: {valid_types}")
        return v

Return Types

Security Constraint: Command return values are serialized to JSON and stored as plaintext in the platform database (result_summary column, truncated to 1024 bytes). Commands MUST NOT return sensitive data in their result dict. Prohibited return values include API keys, credentials, full document bodies, LLM response text, and PII. Return only metadata, identifiers, counts, and safe status values. Violating this constraint exposes user data on any database compromise (see Security Architecture).

Commands can return various types:

Dictionary (most common)

@command("get-stats", namespace="analytics")
async def get_stats(args: Args, ctx: Context) -> dict:
    return {
        "total": 1000,
        "average": 45.5,
        "trend": "up"
    }

Pydantic Model

class StatsResult(BaseModel):
    total: int
    average: float
    trend: str

@command("get-stats", namespace="analytics")
async def get_stats(args: Args, ctx: Context) -> StatsResult:
    return StatsResult(total=1000, average=45.5, trend="up")

String

@command("greet", namespace="example")
async def greet(args: Args, ctx: Context) -> str:
    return f"Hello, {args.name}!"

None (side effects only)

@command("send-notification", namespace="alerts")
async def send_notification(args: Args, ctx: Context) -> None:
    await ctx.email.send(args.recipient, args.message)
    # No return value

Sync vs Async Commands

Use for I/O-bound operations:

@command("fetch-data", namespace="api")
async def fetch_data(args: Args, ctx: Context) -> dict:
    # Async HTTP call
    response = await ctx.http.get(args.url)

    # Async storage
    await ctx.storage.save("cached-data", response)

    return response

Sync

Use for CPU-bound operations:

@command("compute-hash", namespace="crypto")
def compute_hash(args: Args, ctx: Context) -> str:
    # CPU-intensive operation
    return hashlib.sha256(args.data.encode()).hexdigest()

Class-Based Commands

For complex commands requiring lifecycle hooks:

from huitzo_sdk import HuitzoCommand

class ImportCommand(HuitzoCommand):
    """Import data from external source."""

    name = "import"
    namespace = "data"
    version = "1.0.0"

    def configure(self):
        """Called before execution to set options."""
        self.timeout = 7200  # 2 hours
        self.retries = 3
        self.queue = "long"

    def on_start(self, args: ImportArgs):
        """Called when command starts."""
        self.log.info(f"Starting import from {args.source}")

    def on_progress(self, percent: int, message: str):
        """Called to report progress."""
        self.update_state(progress=percent, status=message)

    def on_retry(self, attempt: int, error: Exception):
        """Called before each retry."""
        self.log.warning(f"Retry {attempt}: {error}")

    def on_complete(self, result):
        """Called after successful completion."""
        self.log.info(f"Import complete: {result}")

    def on_error(self, error: Exception):
        """Called on failure."""
        self.log.error(f"Import failed: {error}")
        # Optionally clean up resources

    async def execute(self, args: ImportArgs, ctx: Context) -> dict:
        """Main execution logic."""
        total_rows = 0

        async for batch in self.fetch_batches(args.source):
            await self.process_batch(batch)
            total_rows += len(batch)
            self.on_progress(
                percent=int(total_rows / args.expected_rows * 100),
                message=f"Processed {total_rows} rows"
            )

        return {"rows_imported": total_rows}

Error Handling

Built-in Exceptions

from huitzo_sdk.errors import (
    CommandError,      # General command failure
    ValidationError,   # Input validation failed
    IntegrationError,  # External service failed
    RateLimitError,    # Rate limited
)

@command("example", namespace="demo")
async def example(args: Args, ctx: Context) -> dict:
    # Validation error
    if args.count < 0:
        raise ValidationError(
            field="count",
            value=args.count,
            message="Count must be non-negative"
        )

    # Integration error
    try:
        result = await external_api.call()
    except APIError as e:
        raise IntegrationError(
            service="external-api",
            status_code=e.status,
            message=str(e)
        )

    return result

Custom Exit Codes

@command("check-status", namespace="health")
async def check_status(args: Args, ctx: Context) -> dict:
    status = await check_system()

    if status == "critical":
        raise CommandError(
            message="System is in critical state",
            exit_code=2  # Custom exit code
        )

    return {"status": status}

Command Metadata

Access command metadata at runtime:

@command("self-aware", namespace="meta")
async def self_aware(args: Args, ctx: Context) -> dict:
    return {
        "command_name": ctx.command_name,
        "namespace": ctx.namespace,
        "version": ctx.command_version,
        "correlation_id": ctx.correlation_id,
        "user_id": str(ctx.user_id),
        "tenant_id": str(ctx.tenant_id),
    }

Best Practices

1. Keep Commands Focused

# ✅ Good: Single responsibility
@command("send-email", namespace="notifications")
async def send_email(args: EmailArgs, ctx: Context) -> dict:
    ...

# ❌ Bad: Doing too much
@command("send-everything", namespace="notifications")
async def send_everything(args: Args, ctx: Context) -> dict:
    # Sending email, SMS, Slack, push notifications...

2. Use Meaningful Names

# ✅ Good: Verb-noun pattern
@command("generate-report", namespace="analytics")
@command("validate-data", namespace="etl")
@command("sync-inventory", namespace="warehouse")

# ❌ Bad: Vague names
@command("do-stuff", namespace="misc")
@command("process", namespace="data")

3. Document with Docstrings

@command("calculate-roi", namespace="finance")
async def calculate_roi(args: ROIArgs, ctx: Context) -> dict:
    """Calculate Return on Investment for a given period.

    This command analyzes revenue and costs to compute ROI metrics.
    Results are cached for 24 hours.

    Examples:
        calculate-roi --start-date 2024-01-01 --end-date 2024-12-31
        calculate-roi --department sales --include-projections
    """
    ...

4. Handle Errors Gracefully

@command("safe-operation", namespace="utils")
async def safe_operation(args: Args, ctx: Context) -> dict:
    try:
        result = await risky_call()
        return {"success": True, "data": result}
    except SpecificError as e:
        ctx.log.warning(f"Expected error: {e}")
        return {"success": False, "error": str(e)}
    except Exception as e:
        ctx.log.error(f"Unexpected error: {e}")
        raise CommandError(
            message="Operation failed unexpectedly",
            details={"error_type": type(e).__name__}
        )

See Also