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) |
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¶
- 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" ```
- 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"
- 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)
- 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.
Related Documentation¶
- Context Reference – Full Context API
- MCP Reference – MCP API documentation
- Commands Reference – Command patterns
- Error Handling – Integration errors
- File Storage Backends – Platform file storage configuration
- Secrets Management – Three-tier secrets model
- Configuration – Environment variables