Skip to content
Development
Skill

/output-dev-cost-hooks

Subscribe to cost events (cost:http:request, llm:generation:metering) to forward LLM and API spend to your own observability system. Use when adding cost/spend logging, building a cost observability integration, or forwarding per-request cost data to an external system.

From plugin
output
43854 skills11 agents1 command
Install
$ npx -y skills add growthxai/output --skill output-dev-cost-hooks --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/output-dev-cost-hooks

Context preview

The summary Claude sees to decide when to auto-load this skill.

Subscribe to cost events (cost:http:request, llm:generation:metering) to forward LLM and API spend to your own observability system. Use when adding cost/spend logging, building a cost observability integration, or forwarding per-request cost data to an external system.

SKILL.md

output-dev-cost-hooks.SKILL.md
name: output-dev-cost-hooks
description: Subscribe to cost events (cost:http:request, llm:generation:metering) to forward LLM and API spend to your own observability system. Use when adding cost/spend logging, building a cost observability integration, or forwarding per-request cost data to an external system.
allowed-tools: [Read, Write, Edit, Glob]

Cost Observability Hooks

Overview

This skill documents how to subscribe to Output's cost events so every priced LLM call and every HTTP call with attached cost (via `addRequestCost`, see `output-dev-http-client-create`) can be forwarded to your own observability system — a webhook, a structured log pipeline, a metrics backend, etc. Handler errors are caught and logged by the framework; they never affect the workflow or the request that triggered them.

When to Use This Skill

  • Forwarding per-call spend to an external observability/webhook endpoint
  • Logging cost events as structured fields for a log platform
  • Building alerting or dashboards on top of workflow spend
  • Auditing which workflows/activities are driving cost, in real time rather than after the fact

This skill is about **project-wide hook registration** for cost data already emitted by the framework. To make an HTTP client emit cost in the first place, see `output-dev-http-client-create`.

Setup

1. Create a hook file

// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';

on<HttpRequestCostEvent>('cost:http:request', async event => {
  // handle HTTP cost
});

on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
  // handle LLM cost
});

2. Register the file

Add the file to `outputai.hookFiles` in `package.json`, alongside any existing hook files. Paths are relative to the package root and point at the **built** `.js` output — `npm run output:worker:build` compiles `src/` to `dist/` (per `tsconfig.json`'s `rootDir`/`outDir`), so `src/cost_hooks.ts` is registered as `dist/cost_hooks.js`, not the `.ts` source itself. The worker loads these files at startup.

{
  "outputai": {
    "hookFiles": [
      "node_modules/@outputai/credentials/dist/hooks.js",
      "./dist/cost_hooks.js"
    ]
  }
}

If you'd rather skip the build step, a hook file can also be plain, uncompiled JavaScript registered directly at its `src/` path (`"./src/cost_hooks.js"`) — see `https://docs.output.ai/operations/error-hooks` for that variant. The rest of this skill uses TypeScript + the built-output path, since that's what the framework's own examples (and every other file in a scaffolded project) use.

Events You Can Subscribe To

| Event | Type import | When it fires | Prefer for | |-------|-------------|----------------|-------------| | `llm:generation:metering` | `LLMGenerationMeteringEvent` from `@outputai/llm` | After every LLM generation (text, image, Agent, streaming) that reports usage — including failed calls that got at least partial usage | New LLM cost integrations | | `cost:llm:request` | `LLMUsageEvent` from `@outputai/llm` | Legacy/compatible LLM cost event, same completion path | Existing handlers only — do not use for new work | | `cost:http:request` | `HttpRequestCostEvent` from `@outputai/http` | Only when your code (or a client's `afterResponse` hook) calls `addRequestCost(response, total)` | Non-LLM paid API calls |

Every event carries the same envelope: `eventId` (UUID v4, stable idempotency key), `eventDate` (ms epoch), `activityInfo` and `workflowDetails` (present when emitted from within a step/evaluator), `outputActivityKind`, and `payload` (the event-specific data described above).

Pattern 1: Forward to an external endpoint

Use this when spend needs to reach an external observability system over HTTP. Forward the raw envelope plus payload; redact anything that might carry secrets (API keys or tokens embedded in query strings) before logging or sending the URL.

// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import { credentials } from '@outputai/credentials';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';

// Use plain fetch here, not createKyClient/outputFetch. Handlers run inside
// the emitting step's async context, so a traced client would add its own
// HTTP trace event to that step's trace on every forwarded cost event.
//
// Read the credential lazily with `get`, not `require`, at module scope: a
// hook file is imported at worker startup by an unguarded `await import()`
// (there's no try/catch around it), so a `require()` that throws here takes
// the whole worker down. `get`/`require` also only see workflow-scoped
// credentials from inside an activity — at startup, outside any activity,
// they resolve the global credential set only, so keep this in a global
// (not per-workflow) credential file.
const getWebhookUrl = (): string | undefined => credentials.get('observability.webhook_url') as string | undefined;

const postEvent = async (json: Record<string, unknown>): Promise<void> => {
  const webhookUrl = getWebhookUrl();
  if (!webhookUrl) {
    return;
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  try {
    await fetch(`${webhookUrl}/events`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(json),
      signal: controller.signal
    });
  } finally {
    clearTimeout(timeout);
  }
};

// Strip query strings — some APIs put API keys or tokens there.
const redactUrl = (url: string): string => {
  try {
    const parsed = new URL(url);
    return `${parsed.origin}${parsed.pathname}`;
  } catch {
    return '[unparseable-url]';
  }
};

on<HttpRequestCostEvent>('cost:http:request', async event => {
  if (!event.workflow
Read more
Ships withoutput

The open-source TypeScript framework for building AI workflows and agents. Designed for Claude Code — describe what you want, Claude builds it, with all the best practices already in place. One framework.

Get the whole plugin

Other skills on output.