Integrations Reference

Integrations Reference

Huitzo provides built-in integrations for common services. All integrations are accessed through the Context object and are automatically configured based on platform settings.

Architecture & registry: This page is the SDK-side reference. For the unified registry model, REST contract, ORM schema, migration strategy, security boundaries, and OAuth lifecycle see Integrations Subsystem v2.

Overview

Integration Access Description
LLM ctx.llm AI/LLM completions (OpenAI, Anthropic)
Email ctx.email Send emails (SendGrid, SMTP)
HTTP ctx.http External API requests
Telegram ctx.telegram Send Telegram messages
Files ctx.files Read/write files
Database ctx.db Postgres / MySQL connectors

LLM Integration

The LLM integration provides access to AI language models for text generation, analysis, and structured output.

Supported Providers

Provider Models Environment Variable
OpenAI gpt-4o, gpt-4o-mini, gpt-4-turbo OPENAI_API_KEY
Anthropic claude-3-5-sonnet, claude-3-haiku ANTHROPIC_API_KEY

Basic Completion

# pseudocode — basic LLM completion
response = await ctx.llm.complete(
    prompt="your prompt text",
    model="<model-name>"       # see supported models table above
)
# response is a string containing the LLM output

With System Message

# pseudocode — completion with system message
response = await ctx.llm.complete(
    prompt=user_query,
    system="role/persona instructions for the LLM",
    model="<model-name>"
)

Structured Output (JSON Mode)

Define a Pydantic model for the expected output shape, then pass it as the schema parameter:

# pseudocode — structured output
response = await ctx.llm.complete(
    prompt="your extraction/analysis prompt",
    response_format="json",
    schema=YourPydanticModel,    # validated automatically
    model="<model-name>"
)
# response is a validated instance of YourPydanticModel

Streaming Responses

# pseudocode — streaming completion
async for chunk in ctx.llm.stream(prompt="...", model="<model-name>"):
    # process each chunk as it arrives
    ...

LLM Options

Parameter Type Description
prompt str The user prompt
model str Model identifier (see supported models table)
system str Optional system/role message
temperature float Creativity (0.0–2.0)
max_tokens int Maximum response length
top_p float Nucleus sampling threshold
stop list[str] Stop sequences
response_format str "json" for structured output
schema BaseModel Pydantic model for JSON validation
# pseudocode — using LLM options
response = await ctx.llm.complete(
    prompt="...",
    model="<model-name>",
    temperature=0.7,
    max_tokens=1000,
    ...
)

Token Usage Tracking

Token usage is automatically tracked for billing. Access current usage:

# Tokens are tracked per-command automatically
# View usage via API: GET /api/v1/usage

Error Handling

from huitzo_sdk.errors import LLMError

try:
    response = await ctx.llm.complete(prompt="...", model="gpt-4o")
except LLMError as e:
    ctx.log.error(f"LLM failed: {e.provider} - {e.message}")
    # e.provider: "openai" or "anthropic"
    # e.model: The model that failed
    # e.status_code: HTTP status if applicable

Email Integration

Send emails through configured email providers (SendGrid or SMTP).

Configuration

Variable Required Description
SENDGRID_API_KEY SendGrid API key
SMTP_HOST SMTP server hostname
SMTP_PORT SMTP port (default: 587)
SMTP_USER SMTP username
SMTP_PASSWORD SMTP password
EMAIL_FROM Default sender address

Basic Email

@command("notify", namespace="alerts")
async def notify(args: Args, ctx: Context) -> dict:
    await ctx.email.send(
        to="[email protected]",
        subject="Alert: Action Required",
        body="Your report is ready for review."
    )
    return {"sent": True}

HTML Email

await ctx.email.send(
    to="[email protected]",
    subject="Weekly Report",
    html="""
    <h1>Weekly Report</h1>
    <p>Here are your metrics:</p>
    <ul>
        <li>Revenue: $10,000</li>
        <li>Users: 150</li>
    </ul>
    """,
)

With Attachments

# PDF attachment
pdf_bytes = generate_pdf_report(data)

await ctx.email.send(
    to="[email protected]",
    subject="Your Report",
    body="Please find your report attached.",
    attachments=[
        {
            "filename": "report.pdf",
            "content": pdf_bytes,
            "content_type": "application/pdf"
        }
    ]
)

Multiple Recipients

await ctx.email.send(
    to=["[email protected]", "[email protected]"],
    cc=["[email protected]"],
    bcc=["[email protected]"],
    subject="Team Update",
    body="..."
)

Email Templates

# pseudocode — sending a template-based email
await ctx.email.send_template(
    to="[email protected]",
    template_id="<your-template-id>",    # provider-specific template ID
    template_data={                       # variables to fill in the template
        "name": "...",
        "report_url": "..."
    }
)

HTTP Integration

Make HTTP requests to external APIs with automatic timeout and retry handling.

GET Request

@command("fetch-data", namespace="api")
async def fetch_data(args: Args, ctx: Context) -> dict:
    response = await ctx.http.get(
        "https://api.example.com/data",
        headers={"Authorization": f"Bearer {args.api_key}"}
    )
    return response  # Parsed JSON

POST Request

response = await ctx.http.post(
    "https://api.example.com/submit",
    json={"name": "value", "count": 42}
)

Request Options

response = await ctx.http.get(
    "https://api.example.com/data",
    headers={"X-Custom-Header": "value"},
    params={"page": 1, "limit": 100},
    timeout=60,  # seconds
)

response = await ctx.http.post(
    "https://api.example.com/upload",
    data={"field": "value"},  # Form data
    files={"document": file_bytes},
)

response = await ctx.http.put(
    "https://api.example.com/resource/123",
    json={"updated": True}
)

response = await ctx.http.delete(
    "https://api.example.com/resource/123"
)

Domain Restrictions

Packs can restrict HTTP access to specific domains in the manifest:

# huitzo.yaml
services:
  http:
    required: true
    allowed_domains:
      - "api.example.com"
      - "*.trusted-domain.org"

Relative Paths and base_url

When a tenant configures an HTTP integration with a base_url, packs may pass relative paths to ctx.http.get / post / put / delete and the path is joined onto the integration's base URL. The base URL's host is implicitly allowed in addition to any allowed_domains declared in the manifest, so operators don't need to repeat the host in two places.

Tenants configure base_url (and allowed_domains) when creating the HTTP integration in the dashboard. See Integrations Subsystem v2 for the full configuration contract.

# pseudocode — assumes the tenant's HTTP integration has
# base_url = https://api.weather.example.com
response = await ctx.http.get(
    "/v1/forecast",
    params={"latitude": 37.77, "longitude": -122.42, "current": "temperature_2m"},
)

Absolute URLs always pass through unchanged, so a pack can target additional hosts (subject to allowed_domains) even when a base_url is set. Wildcard entries in allowed_domains (e.g. *.trusted-domain.org) continue to apply independently of base_url — both mechanisms are additive permits, neither replaces the other.

Without a base_url, a relative path raises HTTPSecurityError with the message URL has no valid hostname. so misconfigured packs surface a clear diagnostic.

If the configured base_url itself includes a path component (e.g. https://api.example.com/v2), the relative path is always appended onto that prefix — the SDK normalizes leading slashes so /v1/forecast and v1/forecast both yield https://api.example.com/v2/v1/forecast.

The cloud-metadata blocklist (169.254.169.254, metadata.google.internal, 100.100.100.200) and HTTPS enforcement still take precedence — base_url cannot unblock SSRF targets or downgrade to plain HTTP. See the security threat model in Architecture: Security for the implications of tenants configuring arbitrary HTTPS hosts.

Error Handling

from huitzo_sdk.errors import HTTPError, HTTPSecurityError

try:
    response = await ctx.http.get("https://api.example.com/data")
except HTTPSecurityError as e:
    ctx.log.error(f"Domain not allowed: {e.domain}")
    # e.domain: The blocked domain
    # e.allowed_domains: List of allowed domains
except HTTPError as e:
    ctx.log.error(f"HTTP request failed: {e.status_code}")
    # e.url: Request URL
    # e.method: HTTP method
    # e.status_code: Response status
    # e.response_body: Response body (truncated)

Telegram Integration

Send messages and documents via Telegram bot.

Configuration

Variable Required Description
TELEGRAM_BOT_TOKEN Bot token from @BotFather

Send Message

@command("alert", namespace="notifications")
async def alert(args: Args, ctx: Context) -> dict:
    await ctx.telegram.send(
        chat_id=args.chat_id,  # User or group chat ID
        message="Alert: System status changed to WARNING"
    )
    return {"sent": True}

Formatted Message

await ctx.telegram.send(
    chat_id="123456789",
    message="*Bold* and _italic_ text\n\n`code block`",
    parse_mode="Markdown"  # or "HTML"
)

Send Document

pdf_bytes = generate_report()

await ctx.telegram.send_document(
    chat_id="123456789",
    document=pdf_bytes,
    filename="report.pdf",
    caption="Your weekly report"
)

Send Photo

await ctx.telegram.send_photo(
    chat_id="123456789",
    photo=image_bytes,
    caption="Chart: Revenue over time"
)

Files Integration

Read and write files from user uploads and pack storage.

Backend Abstraction

The ctx.files API is backend-agnostic—your pack code works identically whether the platform uses local filesystem, S3, Azure Blob, or GCS. The storage backend is configured at the platform level, not in pack code.

# This code works with ANY storage backend
@command("process-data", namespace="analytics")
async def process_data(args: Args, ctx: Context) -> dict:
    df = await ctx.files.read_excel(args.file_path)  # Works everywhere
    result = analyze(df)
    await ctx.files.write("output/report.csv", result.to_csv())  # Works everywhere
    return {"status": "complete"}

Pack developers don't need to know or care which backend is configured. See File Storage Backends for platform configuration details.

Read Excel

@command("analyze-excel", namespace="data")
async def analyze_excel(args: Args, ctx: Context) -> dict:
    # Returns pandas DataFrame
    df = await ctx.files.read_excel(args.file_path)

    # Specific sheet
    df = await ctx.files.read_excel(args.file_path, sheet="Sales")

    return {"rows": len(df), "columns": list(df.columns)}

Read CSV

df = await ctx.files.read_csv(args.file_path)
df = await ctx.files.read_csv(args.file_path, delimiter=";", encoding="utf-8")

Read JSON

data = await ctx.files.read_json(args.file_path)

Write Files

# Write CSV
await ctx.files.write("output/report.csv", df.to_csv(index=False))

# Write JSON
await ctx.files.write("output/data.json", json.dumps(data, indent=2))

# Write binary
await ctx.files.write("output/report.pdf", pdf_bytes, binary=True)

File Information

info = await ctx.files.info(args.file_path)
# Returns: {"size": 1024, "created": datetime, "modified": datetime, "type": "xlsx"}

List Files

files = await ctx.files.list("uploads/")
# Returns: [{"name": "data.xlsx", "size": 1024, "modified": datetime}, ...]

File Existence

if await ctx.files.exists("uploads/data.xlsx"):
    df = await ctx.files.read_excel("uploads/data.xlsx")

Get Download URL

Generate a URL for direct file download (useful for returning file links to users):

# Generate presigned URL (for object storage backends)
url = await ctx.files.get_url("output/report.pdf", expires=3600)  # 1 hour
return {"download_url": url}

File Limits

Platform-wide defaults (configurable by administrators):

Limit Default Environment Variable
Max file size 100 MB HUITZO_FILE_MAX_SIZE_MB
Max files per user 1000 HUITZO_FILE_MAX_PER_USER
Tenant storage quota 10 GB HUITZO_FILE_QUOTA_GB

Packs can declare stricter limits in the manifest:

# huitzo.yaml
services:
  files:
    max_size_mb: 50       # Stricter than platform default
    allowed_types:
      - .csv
      - .xlsx
      - .json

Tenant Isolation

Files are automatically isolated by tenant. Pack code uses relative paths:

# Pack writes to "output/report.pdf"
await ctx.files.write("output/report.pdf", data)

# Platform stores at: tenant/{tenant_id}/output/report.pdf
# Other tenants cannot access this file

Integration Permissions

Commands must declare required integrations in the manifest:

# huitzo.yaml
commands:
  - name: "generate-report"
    permissions:
      - "llm:complete"       # LLM completion
      - "llm:stream"         # LLM streaming
      - "email:send"         # Send emails
      - "telegram:send"      # Send Telegram messages
      - "http:request"       # External HTTP requests
      - "files:read"         # Read files
      - "files:write"        # Write files

Best Practices

1. Use Appropriate Models

# ✅ Good: Use smaller model for simple tasks
summary = await ctx.llm.complete(prompt=simple_prompt, model="gpt-4o-mini")

# ✅ Good: Use larger model for complex reasoning
analysis = await ctx.llm.complete(prompt=complex_prompt, model="gpt-4o")

2. Handle Integration Errors

# ✅ Good: Graceful degradation
try:
    await ctx.email.send(to=user_email, subject="Report", body=content)
except EmailError:
    ctx.log.warning("Email failed, saving to storage instead")
    await ctx.storage.save("pending-email", {"to": user_email, "content": content})

3. Respect Rate Limits

# ✅ Good: Batch operations where possible
# Instead of 100 individual LLM calls, batch into fewer calls with more content

4. Secure API Keys

# ❌ Bad: Hardcoded API key
response = await ctx.http.get(url, headers={"Authorization": "Bearer sk-xxx"})

# ✅ Good: Use environment variables (platform secrets)
api_key = ctx.env.require("EXTERNAL_API_KEY")
response = await ctx.http.get(url, headers={"Authorization": f"Bearer {api_key}"})

User Secrets for External Services

When your pack integrates with external services where each user has their own API key (e.g., Plaid, QuickBooks, Stripe Connect), use the ctx.secrets API instead of platform environment variables.

When to Use User Secrets

Scenario Use Access Method
Platform-wide LLM key Platform secret ctx.env.require("OPENAI_API_KEY")
Platform email service Platform secret ctx.env.require("SENDGRID_API_KEY")
User's own CRM API key User secret ctx.secrets.require("CRM_API_KEY")
User's financial data provider User secret ctx.secrets.require("PLAID_API_KEY")

Declaring User Secrets

Declare required user secrets in your pack manifest:

# huitzo.yaml
secrets:
  user_required:
    - name: "PLAID_CLIENT_ID"
      description: "Your Plaid client ID"
      help_url: "https://dashboard.plaid.com/developers/keys"

    - name: "PLAID_SECRET"
      description: "Your Plaid secret key"
      help_url: "https://dashboard.plaid.com/developers/keys"

  user_optional:
    - name: "PREMIUM_DATA_KEY"
      description: "Optional: Premium data provider API key"

Accessing User Secrets

The secrets API has three methods:

Method Behavior Use When
ctx.secrets.require(name) Returns value or raises SecretsError Secret is mandatory for the command
ctx.secrets.get(name) Returns value or None Secret is optional
ctx.secrets.exists(name) Returns bool Checking before conditional logic
# pseudocode — accessing user secrets in a command

# Required secret → raises SecretsError if the user hasn't configured it
api_key = ctx.secrets.require("SECRET_NAME")

# Optional secret → returns None if not set
optional_key = ctx.secrets.get("OPTIONAL_SECRET_NAME")

# Use the credentials with your external service
result = await external_service.call(api_key=api_key, ...)

# Conditionally use optional features
if optional_key:
    result = await enrich_result(result, optional_key)

Error Handling for User Secrets

Provide helpful guidance when secrets are missing or invalid:

# pseudocode — secrets error handling pattern

try:
    api_key = ctx.secrets.require("SECRET_NAME")
except SecretsError:
    # Return user-friendly guidance pointing to Settings → Pack Secrets
    return {"error": "...", "help": "Add your key in Settings → Pack Secrets"}

try:
    data = await external_api.call(api_key, ...)
except AuthenticationError:
    # Wrap as ExternalAPIError with actionable message
    raise ExternalAPIError(service="...", message="Key invalid. Update in Settings → Pack Secrets.")

Best Practices for User Secrets

  1. Minimize required secrets - Only require what's essential

```yaml # ✅ Good: Single required secret secrets: user_required: - name: "API_KEY" description: "Your service API key"

# ❌ Avoid: Too many required secrets secrets: user_required: - name: "API_KEY" - name: "API_SECRET" - name: "API_REGION" - name: "API_VERSION" ```

  1. Provide clear descriptions and help URLs

yaml secrets: user_required: - name: "STRIPE_SECRET_KEY" description: "Your Stripe secret key (starts with sk_)" help_url: "https://dashboard.stripe.com/apikeys"

  1. Use optional secrets for premium features

python if ctx.secrets.exists("PREMIUM_API_KEY"): return await premium_analysis(data) else: return await standard_analysis(data)

  1. Validate credentials early

```python @command("setup", namespace="integration") async def setup(args: Args, ctx: Context) -> dict: """Validate user credentials before first use.""" api_key = ctx.secrets.require("SERVICE_API_KEY")

   # Verify the key works
   valid = await service.validate_key(api_key)
   if not valid:
       raise ExternalAPIError(
           service="service-name",
           message="API key validation failed. Please check your key."
       )

   return {"status": "configured", "valid": True}

```

Related: See Secrets Management for the complete three-tier secrets model.


Database Integration

The database integration lets a pack run parameterised queries against user-registered Postgres or MySQL connectors. Connectors are stored in the unified Integration registry and resolved at command-execution time when the pack manifest declares services: [db].

Architecture: Full lifecycle (create → verify → use → revoke → audit), security boundaries, and the registry contract live in Integrations Subsystem v2.

Declaring DB integrations in the manifest

# huitzo.yaml — pseudocode
services:
  db: {}                     # required to receive ctx.db at runtime
db_integrations:             # optional name allowlist (omit to allow any)
  - "primary"
  - "warehouse"

Querying

# pseudocode — read rows
rows = await ctx.db.query(
    "primary",                                 # integration name
    "SELECT id, email FROM users WHERE id = $1",
    user_id,
    timeout=10,
)
# rows is a list[dict[str, Any]] with one entry per row.

Mutating

# pseudocode — INSERT / UPDATE / DELETE
n = await ctx.db.execute(
    "primary",
    "UPDATE users SET active = $1 WHERE id = $2",
    True, user_id,
)
# n is the integer row count reported by the engine.

Transactions

# pseudocode — multi-statement transaction (commits on clean exit, rolls back on raise)
async with ctx.db.transaction("primary") as txn:
    await txn.execute("INSERT INTO orders (id, total) VALUES ($1, $2)", order_id, 99)
    await txn.execute("UPDATE inventory SET qty = qty - 1 WHERE sku = $1", sku)

Placeholder syntax

Engine Placeholder Example
Postgres (type=postgres) $1, $2, ... WHERE id = $1
MySQL (type=mysql) %s WHERE id = %s

Limits and security

Concern Default How to change
Query timeout 30 s Pass timeout=… (max 300 s)
Result row cap 10 000 (default) Set row_limit on the integration config; max 1 000 000
DDL (CREATE/DROP/ALTER/...) rejected Set allow_ddl: true on the integration
TLS for Postgres sslmode=require verify-ca / verify-full; disable is rejected
Internal hosts (loopback / RFC1918) rejected self-host only — set HUITZO_ALLOW_INTERNAL_HOSTS=true

Error handling

from huitzo_sdk.errors import DatabaseError

try:
    rows = await ctx.db.query("primary", "SELECT 1")
except DatabaseError as e:
    ctx.log.error(f"DB query failed on {e.integration!r}: {e.message}")

DatabaseError is raised for: integration not in the manifest allowlist, DDL violations, timeouts, row-limit overruns, SSRF/TLS gate failures, and all driver-level errors (asyncpg / aiomysql).


MCP Integration

The MCP (Model Context Protocol) integration enables your pack to call tools from external MCP servers. This provides a standardized way to integrate with services like GitHub, PostgreSQL, Slack, and many others.

Overview

Aspect Details
Access ctx.mcp
Configuration mcp_servers section in huitzo.yaml
Transport STDIO (subprocess) or HTTP+SSE (remote)
Discovery Tools discovered automatically via tools/list

Quick Example

from huitzo_sdk import command, Context

@command("github-issue", namespace="devtools")
async def github_issue(args: Args, ctx: Context) -> dict:
    """Create a GitHub issue using MCP."""
    result = await ctx.mcp.call(
        server="github",
        tool="create_issue",
        arguments={
            "owner": args.owner,
            "repo": args.repo,
            "title": args.title,
            "body": args.body
        }
    )
    return {"issue_number": result["number"], "url": result["html_url"]}

Configuration

# huitzo.yaml
mcp_servers:
  - name: github
    type: stdio
    command: ["uvx", "mcp-server-github"]
    env:
      GITHUB_TOKEN: "${secrets.GITHUB_TOKEN}"

secrets:
  user_required:
    - name: "GITHUB_TOKEN"
      description: "Your GitHub personal access token"
      help_url: "https://github.com/settings/tokens"

Error Handling

from huitzo_sdk.errors import MCPError, MCPConnectionError, MCPToolError

try:
    result = await ctx.mcp.call("github", "create_issue", {...})
except MCPConnectionError:
    ctx.log.error("GitHub MCP server unavailable")
    return {"error": "GitHub integration temporarily unavailable"}
except MCPToolError as e:
    ctx.log.error(f"GitHub tool failed: {e.message}")
    raise

Available MCP Servers

Popular MCP servers include:

Server Package Description
GitHub mcp-server-github GitHub API (issues, PRs, repos)
PostgreSQL mcp-server-postgres Database queries
Filesystem mcp-server-filesystem File operations
Slack mcp-server-slack Slack messaging
Brave Search mcp-server-brave-search Web search

See the MCP Server Directory for more options.

Full Reference: See MCP Reference for complete API documentation.