From astropods
Ports a net-new project to the Astropods platform by adding astropods.yml, Dockerfile, and AGENT.md. No agent logic is modified.
How this skill is triggered — by the user, by Claude, or both
Slash command
/astropods:migrate-to-astropodsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill to port a project that does not yet run on Astropods. It adds the files needed to run an existing agent with `ast project start`: `astropods.yml`, `Dockerfile`, and optionally `AGENT.md`. **No agent logic is modified.**
Use this skill to port a project that does not yet run on Astropods. It adds the files needed to run an existing agent with ast project start: astropods.yml, Dockerfile, and optionally AGENT.md. No agent logic is modified.
If the project already runs on Astropods but is missing the platform's telemetry adapter, use wire-astropods-telemetry instead — that skill installs the adapter and wires up serve() so OpenTelemetry traces flow back to the platform.
Confirm the plan before performing any actions.
These trip up nearly every migration. Read before writing any YAML.
agent.interfaces.frontend: true requires the container to listen on port 80. No other port works for the frontend interface.ASTRO_EXTERNAL_AGENT_URL is injected automatically. Read it from env when the agent needs its own public URL (callbacks, redirects, links in emails). Do not declare your own APP_URL input.agent.interfaces.messaging: true is a boolean. It is not the same field as dev.interfaces.messaging.adapters: [web] (which is an object — for local-dev playground). Both can coexist; don't confuse them.knowledge: block entirely. Don't sink time into local postgres debugging when an external option exists.ast dev on Docker Desktop (macOS) cannot reach IPv6-only DB hosts. vpnkit routes only IPv4 to the internet, regardless of daemon IPv6 settings. Supabase's direct connection URL (db.*.supabase.co) is IPv6-only and will fail with gaierror. Use the IPv4 Session/Transaction Pooler endpoints.os.environ["KEY"] (subscript) for required env vars. .get() returns None silently when the var is missing; subscript crashes loudly so you find misconfig immediately instead of debugging a NoneType error five layers deep.asyncpg/psycopg. They embed the full DSN, password included. Log type(e).__name__ instead..strip() connection strings copied from dashboards. A trailing newline causes gaierror: No address associated with hostname — a real time-waster because the URL looks correct in logs.Read the code to understand:
python -m agent.main, bun run agent/index.ts)git, ffmpeg, native binaries)requirements.txt, package.json, or equivalentastropods.ymlLink to the Astropods spec documentation: https://docs.astropods.com/astropods-package-spec
Link to the Astropods spec schema: https://astropods.com/schema/package.json
If the project already includes an astropods.yml, check if it needs to be updated.
# yaml-language-server: $schema=https://astropods.com/schema/package.json
spec: astro/v1
name: "<agent-name>" # kebab-case; org-scoped: "@postman/agent-name"
agent:
build:
context: .
dockerfile: Dockerfile
models:
openai: # include only providers the agent actually uses
provider: openai
anthropic:
provider: anthropic
integrations: # built-in providers that auto-inject credentials
firecrawl:
provider: firecrawl
github:
provider: github
inputs: # everything else — secrets and config
TAVILY_API_KEY:
name: TAVILY_API_KEY
datatype: string
secret: true
description: "Tavily API key for web search"
display-as: short-text # short-text | select | textarea
SOME_OPTION:
name: SOME_OPTION
datatype: string
description: "Which backend to use"
default: "tavily"
optional: true
display-as: select
options: [tavily, openai, none]
dev:
interfaces:
messaging:
adapters: [web] # web | slack (can list both)
Rules:
inputs must be a map (not a list). Each key is the env var name.models, integrations, knowledge, or inputs entirely if not needed.inputs.meta.description does NOT go in astropods.yml — put it in AGENT.md frontmatter (see step 5).agent.interfaces vs dev.interfaces)agent.interfaces declares what the platform exposes in production. dev.interfaces (shown above) controls what ast project start spins up locally. They are different fields with different shapes — easy to confuse.
agent:
build:
context: .
dockerfile: Dockerfile
interfaces:
frontend: true # public frontend served from the container's port 80
messaging: true # boolean — NOT { adapters: [...] }
When frontend: true:
ASTRO_EXTERNAL_AGENT_URL with the public URL. Read this for any callback/redirect/link-in-email need — do not declare your own APP_URL.If the agent already uses a hosted database (Supabase, Neon, RDS, etc.), skip the knowledge: block entirely and declare the connection string as a secret input. The container reads it from env and connects directly. This is by far the simplest path when an external DB is available.
inputs:
POSTGRES_URL:
name: POSTGRES_URL
datatype: string
secret: true
description: "PostgreSQL connection string"
See the troubleshooting table below for the IPv6 caveat when the host name is IPv6-only.
knowledge)If the agent needs persistent storage, declare it under knowledge. The platform manages the service and injects connection details.
knowledge:
db:
provider: postgres # platform-managed postgres; injects POSTGRES_HOST/PORT/USER/PASSWORD/DB
persistent: true
vectors:
provider: qdrant # platform-managed qdrant; injects QDRANT_HOST/PORT/API_KEY
persistent: true
cache:
provider: redis # platform-managed redis; injects REDIS_HOST/PORT/PASSWORD
persistent: true
For vector workloads requiring pgvector, use a custom container instead of provider mode (provider postgres does not include pgvector):
knowledge:
pg:
container:
image: pgvector/pgvector:pg17
port: 5432
volume: /var/lib/postgresql/data
persistent: true
inputs:
- name: POSTGRES_DB
datatype: string
default: mydb
- name: POSTGRES_USER
datatype: string
default: postgres
- name: POSTGRES_PASSWORD
datatype: string
default: postgres
- name: POSTGRES_HOST_AUTH_METHOD
datatype: string
default: trust
- name: PGDATA
datatype: string
default: /var/lib/postgresql/data/pgdata
Container mode does not inject POSTGRES_HOST — the platform injects KNOWLEDGE_{UPPER(name)}_HOST and KNOWLEDGE_{UPPER(name)}_PORT instead (e.g. KNOWLEDGE_PG_HOST). If no env vars are injected, connect using the knowledge key as the hostname (e.g. knowledge-pg).
providers)For third-party services that aren't built-in (Jira, Gong, Salesforce, etc.), use providers to group credentials:
providers:
jira:
scope: [integrations]
variables:
- name: API_KEY
description: Jira API token
datatype: string
secret: true
- name: BASE_URL
description: Jira instance base URL
datatype: string
integrations:
jira:
provider: jira # references the custom provider above
DockerfileMatch the runtime the agent already uses. Don't change how it runs, just containerize it.
Python:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
RUN adduser --disabled-password --uid 1000 agent
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Add system deps if needed:
# RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
COPY . .
USER agent
CMD ["python", "-m", "agent.main"] # ← match the actual entry point
TypeScript/Bun:
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install
COPY . .
FROM oven/bun:1-slim
WORKDIR /app
COPY --from=builder /app ./
# Add system deps if needed:
# RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
RUN chown -R bun:bun /app
USER bun
CMD ["bun", "run", "agent/index.ts"] # ← match the actual entry point
Node.js/npm:
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/ ./
USER node
CMD ["node", "agent/index.js"] # ← match the actual entry point
AGENT.md (recommended)AGENT.md populates the agent's catalog card for discovery. Use YAML frontmatter:
---
description: "One-line summary of what the agent does (max 200 chars)"
tags: [productivity, coding, data]
authors:
- name: Your Name
account: your-github-handle
capabilities:
- "Does X when given Y"
- "Integrates with Z to accomplish W"
repository: github:your-org/your-repo
integrations:
- GitHub
- Slack
---
## Overview
What the agent does and why it's useful.
## Usage
How to interact with it. Example prompts.
## Limitations
Known gaps or constraints.
Run ast project start in the agent directory. It should build the container, start all services, and expose a messaging interface.
Once running, the agent works end-to-end but the platform records no run telemetry. To enable OpenTelemetry traces, follow up with wire-astropods-telemetry.
| Problem | Fix |
|---|---|
cannot unmarshal !!seq into map[string]spec.Input | inputs is a list — convert to map syntax (step 2) |
| Container exits immediately | Check CMD matches the real entry point |
| Missing system dep | Add apt-get install to the runtime stage of the Dockerfile |
Agent can't find GRPC_SERVER_ADDR | Normal outside ast project start — it's injected automatically by the runner |
Postgres container fails: POSTGRES_PASSWORD not specified | Add POSTGRES_HOST_AUTH_METHOD: trust and PGDATA: /var/lib/postgresql/data/pgdata to knowledge inputs; avoid secret: true on inputs that need defaults |
Postgres container fails: chmod Operation not permitted | Set PGDATA to a subdirectory (e.g. /var/lib/postgresql/data/pgdata) so postgres creates it with correct ownership instead of chmod-ing the mount root |
Native module missing (e.g. tokenizers-linux-arm64-gnu) | Add --platform=linux/amd64 to both Dockerfile FROM lines to force x86_64 if the package has no ARM64 binary |
secret: true input default not applied by platform | Remove secret: true for internal service credentials (like postgres password) where the default should always apply; use secret: true only for user-supplied credentials |
| Container mode postgres host unknown | Platform injects KNOWLEDGE_{UPPER(name)}_HOST/PORT (e.g. KNOWLEDGE_PG_HOST). If not injected, use the knowledge key name as the hostname (e.g. knowledge-pg) |
ast dev can't reach external DB host (gaierror, "No address associated with hostname") | (1) Check for trailing whitespace in the connection URL — strip it. (2) If the host is IPv6-only (e.g. Supabase's db.*.supabase.co direct URL), Docker Desktop on macOS cannot route it via vpnkit even with daemon IPv6 enabled. Switch to an IPv4 endpoint (Supabase Session Pooler on port 5432 or Transaction Pooler on 6543). |
provider: postgres starts the container but POSTGRES_HOST/USER/PASSWORD/DB never reach the agent | Provider mode injection has been observed to silently fail in some environments. Fall back to container mode using the image: postgres:16 recipe above with POSTGRES_HOST_AUTH_METHOD: trust and a PGDATA subdirectory. |
npx claudepluginhub astropods/agents --plugin astropodsCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.