---
title: "Standalone TypeScript"
description: "Use evlog in standalone TypeScript scripts, CLI tools, queues, cron jobs, and any TypeScript process."
canonical_url: "https://www.evlog.dev/integrate/frameworks/standalone"
---
# Standalone TypeScript

> Use evlog in standalone TypeScript scripts, CLI tools, queues, cron jobs, and any TypeScript process.

For scripts, CLI tools, queue workers, cron jobs, and any TypeScript process that doesn't use a web framework, evlog provides `createLogger` and `createRequestLogger` from the core package.

<callout color="neutral" icon="i-lucide-globe">

For scripts, queue workers, cron, and CLIs, this page is the reference. On Cloudflare Workers, prefer [Cloudflare Workers](https://www.evlog.dev/integrate/frameworks/cloudflare-workers) (`createWorkersLogger`).

</callout>

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my TypeScript project" icon="i-simple-icons-typescript">

Set up evlog in my TypeScript project for scripts, workers, or CLI tools.

- Install evlog: pnpm add evlog
- Import initLogger and createLogger (or createRequestLogger) from 'evlog'
- Call initLogger({ env: { service: 'my-script' } }) once at startup
- Create a logger per logical operation with createLogger({ jobId, source })
- Use log.set() to accumulate context as the operation progresses
- Call log.emit() manually when the operation completes

Docs: [https://www.evlog.dev/integrate/frameworks/standalone](https://www.evlog.dev/integrate/frameworks/standalone)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog
```

```bash [bun]
bun add evlog
```

```bash [yarn]
yarn add evlog
```

```bash [npm]
npm install evlog
```

</code-group>

### 2. Initialize and create loggers

```typescript [scripts/sync-job.ts]
import type { DrainContext } from 'evlog'
import { initLogger, log, createLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 10 } })
const drain = pipeline(createAxiomDrain())

initLogger({
  env: { service: 'my-script', environment: 'production' },
  drain,
})

// Every log is automatically drained
log.info({ action: 'sync_started' })

const syncLog = createLogger({ jobId: 'sync-001', source: 'postgres', target: 's3' })
syncLog.set({ recordsSynced: 150 })
syncLog.emit() // drained automatically

// Flush remaining events before exit
await drain.flush()
```

<callout color="info" icon="i-lucide-info">

Always call `drain.flush()` before the process exits to ensure all buffered events are sent.

</callout>

<callout color="info" icon="i-custom-vite">

**Using vite-node?** The [`evlog/vite` plugin](https://www.evlog.dev/reference/vite-plugin) replaces the `initLogger()` call with compile-time auto-initialization, strips `log.debug()` from production builds, and injects source locations.

</callout>

## createLogger vs createRequestLogger

evlog provides two manual logger constructors:

**createLogger(context)** - For non-HTTP contexts (scripts, CLI, queues):

```typescript [scripts/job.ts]
import { createLogger } from 'evlog'

const log = createLogger({ jobId: 'migrate-001', source: 'postgres' })
log.set({ recordsProcessed: 500 })
log.emit()
```

**createRequestLogger(requestMeta)** - For HTTP-like contexts where you want method/path/status tracking:

```typescript [scripts/webhook-handler.ts]
import { createRequestLogger } from 'evlog'

const log = createRequestLogger({
  method: 'POST',
  path: '/webhook/stripe',
})
log.set({ event: 'invoice.paid', customerId: 'cus_123' })
log.emit()
```

Both require manual `log.emit()` calls since there is no automatic lifecycle to hook into.

## Wide Events

Build up context progressively, then emit:

```typescript [scripts/migrate-users.ts]
import { initLogger, createLogger } from 'evlog'

initLogger({
  env: { service: 'migrate' },
})

const log = createLogger({ task: 'user-migration' })

const users = await db.query('SELECT * FROM legacy_users')
log.set({ found: users.length })

let migrated = 0
for (const user of users) {
  await newDb.upsert({ id: user.id, email: user.email, plan: user.plan })
  migrated++
}

log.set({ migrated, status: 'complete' })
log.emit()
```

```bash [Terminal output]
14:58:15 INFO [migrate] user-migration
  ├─ migrated: 1250
  ├─ found: 1250
  ├─ status: complete
  └─ task: user-migration
```

## Error Handling

Use `createError` for structured errors:

```typescript [scripts/sync-job.ts]
import { createError, parseError } from 'evlog'

try {
  const result = await externalApi.sync()
  if (!result.ok) {
    throw createError({
      message: 'Sync failed',
      why: `API returned ${result.status}`,
      fix: 'Check the API status page and retry',
    })
  }
} catch (error) {
  log.error(error instanceof Error ? error : new Error(String(error)))
  log.emit()

  const { message, why, fix } = parseError(error)
  console.error(`${message}\nWhy: ${why}\nFix: ${fix}`)
  process.exit(1)
}
```

## Configuration

A small script can keep its options inline, as above. For anything bigger, move them to an `evlog.config.ts` next to the `package.json`, the file the CLI reads too, and pass it to `initLogger`:

```typescript [src/index.ts]
import { initLogger, toLoggerConfig } from 'evlog'
import config from '../evlog.config'

initLogger(toLoggerConfig(config))
```

`toLoggerConfig` keeps every option `initLogger` takes, such as the service, sampling, `minLevel`, `redact`, the drain and the plugins. The file's `enrich`, `keep`, `include`, `exclude` and `routes` are framework middleware options, so they do not apply to a script.

## Drain & Enrichers

Configure drain in `initLogger`:

```typescript [scripts/init-logger.ts]
import type { DrainContext } from 'evlog'
import { initLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

initLogger({
  env: { service: 'my-script' },
  drain,
})
```

<callout color="info" icon="i-lucide-info">

See the [Adapters](https://www.evlog.dev/integrate/adapters/overview) docs for all available drain adapters (Axiom, OTLP, PostHog, Sentry, Better Stack).

</callout>

<callout color="neutral" icon="i-lucide-arrow-right">

See the full [bun-script example](https://github.com/evloghq/evlog/tree/main/examples/bun-script) for a complete working script.

</callout>

## Next Steps

- [Node.js request logging](https://www.evlog.dev/use-cases/structured-logging-nodejs): apply these primitives in a complete HTTP server and trace success and failure from the response to the log.
- [Wide Events](https://www.evlog.dev/learn/wide-events): Design comprehensive events with context layering
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): Send logs to Axiom, Sentry, PostHog, and more
- [Sampling](https://www.evlog.dev/learn/sampling): Control log volume with head and tail sampling
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Throw errors with `why`, `fix`, and `link` fields


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
