Use Pi from Inngest safely in joelclaw. Covers event-driven detached Pi CLI runners, direct pi-ai calls inside step.run, service-account OAuth auth, claim-check files, and when to choose each pattern.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add joelhooks/joelclaw --skill pi-inngest --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Pi Inngest?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/joelhooks-pi-inngest)More formats (shields.io, HTML) on the badges page.
---
name: pi-inngest
displayName: Pi + Inngest
description: Use Pi from Inngest safely in joelclaw. Covers event-driven detached Pi CLI runners, direct pi-ai calls inside step.run, service-account OAuth auth, claim-check files, and when to choose each pattern.
version: 0.1.0
author: joel
tags:
- pi
- inngest
- workflows
- service-account
- inference
---
# Pi + Inngest
Use this when implementing, reviewing, or debugging joelclaw flows where Inngest orchestrates work and Pi provides model execution, tool execution, or harness-native agent behavior.
This skill is joelclaw-specific. It assumes Inngest and Pi are first-class local infrastructure, not incidental dependencies.
## When to Use
Load this skill when the task mentions:
- Pi inside Inngest
- `pi-ai`, `@earendil-works/pi-ai`, or legacy `@mariozechner/pi-ai`
- launching `pi` from an Inngest function
- `step.waitForEvent` around agent work
- detached Pi CLI children
- link ingest `pi/workflow.*` events
- service-account Pi auth
- OpenAI Codex OAuth/subscription auth from Pi
- choosing between Pi CLI and direct `pi-ai`
Also use it when an Inngest function needs LLM context work and the current path is drifting into ad hoc API keys, polling loops, or mystery subprocesses.
## Core Rule
Inngest owns durability. Pi owns agent/model execution.
Do not blur that boundary:
- Inngest functions should emit and wait on events, persist claim-check files, and record terminal outcomes.
- Pi CLI should be treated as an external actor, not an in-process library.
- Direct `pi-ai` is allowed only for bounded model calls where Pi session/harness behavior is not needed.
- Never smuggle raw model API keys into a Pi subprocess when machine OAuth auth is available.
## Decision Guide
| Use this | When | Avoid when |
|---|---|---|
| Direct `pi-ai` inside `step.run` | Short deterministic source-context calls, classification, extraction repair, summary/context generation, no Pi tools/session needed | Need Pi tools, skills, extension behavior, session artifacts, long-running agent work, or isolation from the worker process |
| Detached Pi CLI + event result | Need real Pi harness behavior, tool policy, session logs, read/write tools, long-running agent work, or process isolation | Tiny model-only calls where `pi-ai` would be simpler and faster |
| Synchronous `pi -p` inside one `step.run` | Tiny smoke/probe only | Production long-running work; this loses the event-driven shape and can wedge a step |
| Polling result files with `step.sleep` | Temporary proof shim only | Production orchestration; prefer `step.waitForEvent` plus a timeout watchdog |
Bench receipt from 2026-06-20:
- Direct `pi-ai` in `step.run`: 2/2 ok, ~5.5-6.5s wall, usage surfaced around 368 tokens / ~$0.0077 per call.
- Detached Pi CLI + result event: 2/2 ok, ~8.0-9.1s wall, wrote `pi-session/`, `pi-output.txt`, and `pi-result.json`.
- Receipt root: `/Users/joel/.local/state/joelclaw-central/tmp/pi-runner-bench/e36f592d-786a-4c15-ba1d-affb5820bbdf`.
Takeaway: use direct `pi-ai` for bounded source-context summarization. Use detached Pi CLI when harness parity, tools, session artifacts, or isolation matter.
## Service Account Auth Contract
joelclaw Central services should use the macOS `joelclaw` service account for durable machine-owned auth where practical.
Current local facts:
- Central Inngest runs as `com.joelclaw.central.inngest` under user `joelclaw`.
- Joel-user clients/canaries may still run as Joel when they write Joel-side Brain files.
- Pi auth is file-backed by `auth.json` in Pi's agent dir.
- Pi agent dir is resolved by `PI_CODING_AGENT_DIR`; otherwise Pi uses `~/.pi/agent` for the current OS user.
- The verified model lane for this work is `openai-codex/gpt-5.6-sol`.
For durable service access:
1. Prefer a real service-owned Pi agent dir, usually:
```txt
/Users/joelclaw/.pi/agent
```
2. Set this explicitly in launchd/env for any service that calls Pi or `pi-ai`:
```sh
PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent
```
3. Keep `auth.json` owned by `joelclaw` and mode `0600` or stricter. Never print it.
4. Verify auth without printing tokens:
```sh
sudo -u joelclaw \
PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent \
node --input-type=module <<'NODE'
import { AuthStorage, ModelRegistry } from '@earendil-works/pi-coding-agent';
import { getModel } from '@earendil-works/pi-ai';
const provider = 'openai-codex';
const model = getModel(provider, 'gpt-5.6-sol');
const registry = ModelRegistry.create(AuthStorage.create());
const auth = await registry.getApiKeyAndHeaders(model);
console.log(JSON.stringify({
provider,
status: registry.getProviderAuthStatus(provider),
ok: auth.ok,
hasApiKey: Boolean(auth.apiKey),
hasHeaders: Boolean(auth.headers && Object.keys(auth.headers).length),
error: auth.error,
}, null, 2));
NODE
```
Expected safe shape:
```json
{
"provider": "openai-codex",
"status": { "configured": true, "source": "stored" },
"ok": true,
"hasApiKey": true
}
```
Do not rely on Joel's personal `~/.pi/agent/auth.json` from a Central service unless the service intentionally runs as Joel. Do not copy auth across accounts casually. If a one-time migration is unavoidable, record it as an explicit operator action, preserve `0600` permissions, and verify via the safe status check above.
### Bridging Joel auth to the service account
Best options, in order:
1. **Independent service login**: run Pi OAuth login under `joelclaw` with `PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent`. This is cleanest but requires an interactive browser/device-code flow.
2. **One-time provider bridge**: copy only the `openai-codex` OAuth credential from Joel's Pi auth store into the `joelclaw` Pi auth store. This grants the service account Joel's subscription-backed Codex access, so treat it as a privileged operator action.
3. **Do not symlink auth files** across accounts. Shared lock files, refresh races, ownership, and `/Users/joel` permissions make this fucky and fragile.
Repo helper:
```sh
cd /Users/joel/Code/joelhooks/joelclaw-central
scripts/setup-pi-service-auth.sh --dry-run
sudo scripts/setup-pi-service-auth.sh --apply
sudo scripts/setup-pi-service-auth.sh --test
```
The helper:
- copies only the `openai-codex` OAuth credential;
- installs a service-readable Pi package wrapper at `/Users/Shared/joelclaw/bin/pi` because Joel's personal global Pi install lives under `/Users/joel/.local/share`, which the service account should not depend on;
- sets and tests `PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent`;
- verifies both `AuthStorage`/`ModelRegistry` and the Pi CLI as `joelclaw` without printing tokens.
## Utah Sidecar Lesson
Utah's `sidecar-management` skill has the right shape: small dynamic Inngest functions live in a watched workspace, reconnect automatically, and notify the main agent by event. Steal the shape, not the names.
For joelclaw, the equivalent doctrine is:
- keep core runtime stable;
- let small event handlers/workflows be loaded as sidecar-style functions where practical;
- one function per file;
- no top-level side effects;
- watcher/reconnect beats manual restart for operator iteration;
- functions communicate by events and claim-check paths, not direct session poking;
- use structured logs, because sidecar bugs without logs are archaeology.
This pairs naturally with Pi-Inngest: sidecar functions can choose direct `pi-ai` for small model steps or detached Pi CLI for harness-native work.
## Pattern A: Direct `pi-ai` in `step.run`
Use this for bounded model calls where a Pi session is unnecessary.
### Implementation Skeleton
Declare package dependencies in the package doing the work. Do not import from a global absolute install path in production code; scratch tests may do that only as a receipt-gathering hack.
```ts
import { Inngest } from 'inngest';
import { complete, getModel } from '@earendil-works/pi-ai';
import { AuthStorage, ModelRegistry } from '@earendil-works/pi-coding-agent';
const inngest = new Inngest({ id: 'joelclaw-example' });
async function runPiAiSummary(prompt: string) {
const model = getModel('openai-codex', 'gpt-5.6-sol');
const registry = ModelRegistry.create(AuthStorage.create());
const auth = await registry.getApiKeyAndHeaders(model);
if (!auth.ok || !auth.apiKey) {
throw new Error(auth.error || 'missing openai-codex auth');
}
const result = await complete(
model,
{
systemPrompt: 'Return concise, grounded JSON only.',
messages: [{ role: 'user', content: prompt, timestamp: Date.now() }],
tools: [],
},
{ apiKey: auth.apiKey, headers: auth.headers },
);
const text = result.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('');
return {
text,
usage: result.usage,
stopReason: result.stopReason,
};
}
export const summarize = inngest.createFunction(
{ id: 'summarize-with-pi-ai', retries: 2, triggers: [{ event: 'source/summary.requested' }] },
async ({ event, step }) => {
return await step.run('complete-pi-ai', async () =>
runPiAiSummary(event.data.prompt),
);
},
);
```
### Rules
- Keep prompts bounded and deterministic.
- Keep calls inside `step.run` idempotent or make output writes idempotent.
- Surface `usage` and `stopReason` in receipts when useful.
- Do not use this path if the work needs Pi tools, filesystem edits via Pi, skills, extensions, or a Pi session log.
- Do not manually read or print `auth.json`; use `AuthStorage` / `ModelRegistry` and print only booleans/status.
## Pattern B: Detached Pi CLI + Event Result
Use this for real Pi harness work.
The shape:
1. Inngest receives `pi/workflow.requested`.
2. A short `step.run` writes `pi-request.json` and spawns a detached child/wrapper.
3. The wrapper invokes `pi -p @pi-prompt.md` with a filtered environment.
4. The child writes `pi-result.json` and artifacts under a claim-check directory.
5. The wrapper emits `pi/workflow.result.ready` with `{ workflowId, sourceId, resultPath }`.
6. The parent function uses `step.waitForEvent` to wait for that result.
7. A result step reads `pi-result.json` and emits one of:
- `pi/workflow.context.completed`
- `pi/workflow.needs_human`
- `pi/workflow.failed`
### Event Names
Use explicit event names. For link ingest, prefer:
```txt
pi/workflow.requested
pi/workflow.result.ready
pi/workflow.context.completed
pi/workflow.needs_human
pi/workflow.failed
```
The wait event must match a stable unique ID, usually `data.workflowId`.
Do not match only `sourceId` when multiple workflows can run for the same source.
### Inngest Skeleton
```ts
export const piWorkflowCli = inngest.createFunction(
{
id: 'pi-workflow-cli',
retries: 0,
triggers: [{ event: 'pi/workflow.requested' }],
},
async ({ event, step }) => {
const { workflowId, sourceId } = event.data;
await step.run('start-detached-pi-cli', async () => {
const requestPath = await writePiRequestClaimCheck(event.data);
spawnDetachedPiWrapper({ workflowId, sourceId, requestPath });
return { workflowId, sourceId, requestPath };
});
const resultEvent = await step.waitForEvent('wait-for-pi-workflow-result', {
event: 'pi/workflow.result.ready',
timeout: '4m',
match: 'data.workflowId',
});
if (!resultEvent) {
const failure = await step.run('write-timeout-failure', async () =>
writePiWorkflowFailureContext({ workflowId, sourceId, reason: 'timeout' }),
);
await step.sendEvent('emit-pi-workflow-failed', {
name: 'pi/workflow.failed',
data: failure,
});
return failure;
}
const result = await step.run('read-pi-result', async () =>
readJson(resultEvent.data.resultPath),
);
await step.sendEvent('emit-terminal-pi-event', routePiResultToEvent(result));
return result;
},
);
```
### Pi Wrapper Rules
- Use `spawn`, not `execFile`, for long Pi sessions. `execFile(pi)` has hung under Node pipe capture on this machine.
- Detach the child from the Inngest function process when the work may be long.
- Give the Pi subprocess only the env it needs:
- `HOME`
- `USER` / `LOGNAME`
- `PATH`
- `SHELL`
- `PI_CODING_AGENT_DIR`
- harmless display/color/session env if needed
- Do not pass `INNGEST_EVENT_KEY`, webhook secrets, Discord tokens, or broad service env into the `pi` subprocess.
- The wrapper may hold Inngest dispatch auth to emit `pi/workflow.result.ready`; the Pi CLI child should not need it.
- Write all large or detailed outputs as files, then send event claim-check paths.
### CLI Invocation Defaults
For link-ingest style context work:
```sh
pi \
--model openai-codex/gpt-5.6-sol \
--thinking low \
--no-extensions \
--no-context-files \
--no-skills \
--no-prompt-templates \
--no-themes \
--session-dir "$WORKFLOW_DIR/pi-session" \
--name "Link Ingest Context" \
--tools read \
-p "@$WORKFLOW_DIR/pi-prompt.md"
```
Tighten or expand tools intentionally. Default to read-only for source-context work.
## Failure Handling
Every Pi-Inngest path needs a terminal event.
- Direct `pi-ai` errors should fail the step or emit a typed failure event with a claim-check receipt.
- Detached Pi CLI start failures must write failure context and emit `pi/workflow.failed`.
- Timeout failures must write a timeout receipt before emitting failure.
- Global Inngest failure handlers can add observability, but they are not a replacement for workflow-level terminal events.
For Discord-facing flows, keep progress quiet:
- registry/manifest progress only during work.
- Discord gets terminal indexed/failed or focused HITL only.
## Testing Checklist
Before calling a Pi-Inngest implementation done:
1. Verify service auth without printing secrets.
2. Run a direct `pi-ai` smoke if that path is used.
3. Run a detached Pi CLI child smoke if that path is used.
4. Prove `step.waitForEvent` resumes from `pi/workflow.result.ready`.
5. Prove timeout writes a failure claim-check and emits terminal failure.
6. Verify the Pi child environment does not include unnecessary service secrets.
7. Check logs/receipts and keep reusable findings in Brain.
Useful local receipts from the link-ingest runner work live in:
```txt
/Users/joel/.brain/projects/joelclaw-link-ingest-2026-06-17.svx
/Users/joel/Code/joelhooks/joelclaw-central/.brain/projects/link-ingest-router-2026-06-17.svx
```
## Documentation Rule
Use this skill for reusable implementation doctrine.
Use Brain for live project memory and receipts.
Use repo scripts/tests for executable proofs.
Do not bury operational truth only in chat transcripts. Chat is compost, not storage. 🐀
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!