by inngest
Universally Triggered Agent Harness - An OpenClaw-like Inngest-powered personal agent
# Add to your Claude Code skills
git clone https://github.com/inngest/utahLast scanned: 5/30/2026
{
"issues": [],
"status": "PASSED",
"scannedAt": "2026-05-30T16:30:45.787Z",
"npmAuditRan": true,
"pipAuditRan": true
}utah is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by inngest. Universally Triggered Agent Harness - An OpenClaw-like Inngest-powered personal agent. It has 132 GitHub stars.
Yes. utah passed SkillsLLM's automated security scan — a dependency vulnerability audit plus prompt-injection heuristics — with no high-severity issues. You can read the full report in the Security Report section on this page.
Clone the repository with "git clone https://github.com/inngest/utah" and add it to your Claude Code skills directory (see the Installation section above).
utah is primarily written in TypeScript. It is open-source under inngest on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh utah against similar tools.
No comments yet. Be the first to share your thoughts!
Universally Triggered Agent Harness
A durable AI agent built with Inngest and pi-ai. No framework. Just a think/act/observe loop — Inngest provides durability, retries, and observability, while pi-ai provides a unified LLM interface across providers.
Simple TypeScript that gives you:
connect(), no server neededChannel (e.g. Telegram) → Inngest Cloud (webhook + transform) → WebSocket → Local Worker → LLM (Anthropic/OpenAI/Google) → Reply Event → Channel API
The worker connects to Inngest Cloud via WebSocket. No public endpoint. No ngrok. No VPS. Messages flow through Inngest as events, and the agent processes them locally with full filesystem access.
The core agent handles conversations. But conversations are ephemeral — the agent forgets, the process restarts, the context window rolls over. The sidecar is what makes Utah's output durable.
A separate process (utah-sidecar) dynamically loads Inngest functions from disk, connects to Inngest Cloud via WebSocket, and runs them independently. The agent can write a new .ts file to the functions directory and the sidecar hot-reloads it automatically — no restart, no deploy, no human intervention.
The key idea: the agent doesn't just run inside loops — it authors new loops and deploys them to the orchestration engine. Each deployed function is a durable skill that runs on its own schedule, with its own retry logic, completely independent of whether the agent is in a conversation.
┌─────────────────────┐ ┌───────────────────────┐
│ Core Agent │ │ Sidecar │
│ app: "ai-agent" │ │ app: "utah-sidecar" │
│ │ │ │
│ handleMessage │ │ workspace/functions/│
│ sendReply │ │ *.ts (dynamic) │
│ subAgent │ │ + heartbeat (auto) │
│ etc. │ │ + file watcher │
└────────┬────────────┘ └────────┬──────────────┘
│ │
│ connect() via WebSocket │
└──────────┬──────────────────┘
│
┌────────▼────────┐
│ Inngest Cloud │
│ events, crons, │
│ retries, state │
└─────────────────┘
Both processes connect to Inngest independently. They share nothing except the event bus.
workspace/functions/*.ts, dynamically imports each file, and registers the exported Inngest functionsfs.watch() monitors the directory — on any change, a 2-second debounce fires, the existing WebSocket closes, functions are re-imported with cache-busting, and a new connection opensThe agent can author new Inngest functions — cron jobs, event handlers, multi-step workflows — by writing a .ts file to workspace/functions/. The sidecar deploys them automatically.
Some example functions that the main agent might write to extend itself: morning-triage, daily-meeting-digest, nightly-workspace-commit, weekly-review. You can also create "loops" with review functions that use LLMs to review and iterate on functions, for example: inbox-triage-review, cold-email-learner.
Each function is durable — retried on failure, observable in the Inngest dashboard, independently scheduled. Skills compound. The agent builds infrastructure for itself.
Agent skills are markdown reference docs (with name/description frontmatter) that appear in the agent's system prompt. The agent can create its own skills to persist knowledge across conversations.
This creates a self-referential system:
The agent is ephemeral. Its output is durable.
Sidecar functions talk back to the main agent by sending agent.message.received events:
await step.sendEvent("alert-agent", {
name: "agent.message.received",
data: {
channel: "system",
sessionKey: "system-alerts",
message: "Alert: something needs attention",
},
});
This means a cron job can monitor something, detect a problem, and start a conversation with the agent — which can then use its tools to investigate and respond. The loops feed each other.
git clone https://github.com/inngest/utah
cd utah
npm install # or pnpm
cp .env.example .env
Edit .env with your keys:
ANTHROPIC_API_KEY=sk-ant-...
INNGEST_EVENT_KEY=...
INNGEST_SIGNING_KEY=signkey-prod-...
Then add the environment variables for your channel(s) — see setup guides below.
Start the worker:
# Production mode (connects to Inngest Cloud via WebSocket)
npm start
# Development mode (uses local Inngest dev server)
npx inngest-cli@latest dev &
npm run dev
On startup, the worker automatically sets up webhooks and transforms for each configured channel.
The agent supports multiple messaging channels. Each channel has its own setup guide:
src/
├── worker.ts # Entry point — connect() or serve()
├── client.ts # Inngest client
├── config.ts # Configuration from env vars
├── agent-loop.ts # Core think → act → observe cycle
├── setup.ts # Channel setup orchestration
├── lib/
│ ├── llm.ts # pi-ai wrapper (multi-provider: Anthropic, OpenAI, Google)
│ ├── tools.ts # Tool definitions (TypeBox schemas) + execution
│ ├── context.ts # System prompt builder with workspace file injection
│ ├── session.ts # JSONL session persistence
│ ├── memory.ts # File-based memory system (daily logs + distillation)
│ └── compaction.ts # LLM-powered conversation summarization
├── functions/
│ ├── message.ts # Main agent function (singleton + cancelOn)
│ ├── send-reply.ts # Channel-agnostic reply dispatch
│ ├── acknowledge-message.ts # Message acknowledgment (typing indicator, etc.)
│ ├── heartbeat.ts # Cron-based memory maintenance
│ └── failure-handler.ts # Global error handler with notifications
└── channels/
├── types.ts # ChannelHandler interface
├── index.ts # Channel registry
├── setup-helpers.ts # Inngest REST API helpers for webhook setup
└── <channel-name>/ # A channel implementation (see README for setup)
├── handler.ts # ChannelHandler implementation
├── api.ts # API client
├── setup.ts # Webhook setup automation
├── transform.ts # Webhook transform
└── format.ts # Formatting for channel messages
workspace/ # Agent workspace (persisted across runs)
├── SOUL.md # Agent personality and behavioral guidelines
├── USER.md # User information
├── MEMORY.md # Long-term memory (agent-writable)
├── memory/ # Daily logs (YYYY-MM-DD.md, auto-managed)
└── sessions/ # JSONL conversation files (gitignored)
The core is a while loop where each iteration is an Inngest step:
step.run("think") calls the LLM via pi-ai's complete()step.run("tool-read")Inngest auto-indexes duplicate step IDs in loops (think:0, think:1, etc.), so you don't need to track iteration numbers in step names.
One incoming message triggers multiple independent functions:
| Function | Purpose | Config | |