# Jobs, Scripts, and Mail

Iwf includes small app-level building blocks for background work, maintenance
scripts, and mail rendering. They are intentionally explicit: the app owns
process supervision, transports, and persistence decisions.

## 1. Background Jobs

Use the canonical queue table SQL:

```idris
schemaSql : String
schemaSql = jobQueueSchemaSql
```

Database operations are exposed as SQL helpers:

- `enqueueJobSql`
- `enqueueIdempotentJobSql`
- `claimNextJobSql`
- `succeedJobSql`
- `retryJobSql`
- `failJobSql`
- `markDeadJobsSql`
- `timeoutRunningJobsSql`
- `jobDashboardQuerySql`
- `deadJobsQuerySql`

Claims use `FOR UPDATE SKIP LOCKED` so multiple workers can pull jobs safely.
Idempotent enqueue uses an optional `idempotency_key` unique column plus
`ON CONFLICT DO NOTHING`, so repeated enqueue attempts can be made harmless by
using the same application-level key.

## 2. Job Runner

Register handlers:

```idris
sendWelcomeEmail : JobPerformer
sendWelcomeEmail job =
  -- decode job.payload and send mail
  pure (Right ())

handlers : List JobHandler
handlers =
  [ jobHandler "send-welcome-email" sendWelcomeEmail ]
```

Run one claim:

```idris
main : IO ()
main = do
  _ <- runJobRunnerOnce defaultJobRunnerConfig handlers
  pure ()
```

`runJobRunnerOnce` runs queue maintenance, claims a queued job, finds a matching
handler by job name, runs it, and marks the job succeeded, queued for retry, or
failed. Maintenance marks exhausted jobs failed, optionally fails timed-out
running jobs, and requeues stale `running` jobs whose lock is older than
`staleTimeoutSeconds`.

Run a supervised worker loop:

```idris
main : IO ()
main = do
  control <- startJobRunner 1 runnerConfig handlers
  -- call requestJobWorkerStop control from your supervisor during shutdown
  pure ()
```

Use `runJobRunnerLoopWithControl` directly when your process supervisor owns the
thread. The loop checks `JobWorkerControl` before every claim, sleeps for the
normalized poll interval, and exits cleanly after `requestJobWorkerStop`.

Tune runner timing:

```idris
runnerConfig : JobRunnerConfig
runnerConfig =
  MkJobRunnerConfig defaultDatabase (MkJobBackoffPolicy 60 2 3600) 300 (Just 3600) stdoutJobLogger
```

The second field is the retry backoff policy: initial delay in seconds,
multiplier, and maximum delay in seconds. The third field is the stale lock
timeout in seconds. The fourth field is an optional running-job timeout; use
`Nothing` when stale recovery should requeue old locks instead of failing them.
The final field is a structured worker logger; use `quietJobLogger` for silent
workers or `stdoutJobLogger` for simple process logs.

Use `runJobRunnerOnceWithDiagnostics` when supervisors need the structured
outcome for a single iteration:

```idris
diagnostics : IO JobRunDiagnostics
diagnostics =
  runJobRunnerOnceWithDiagnostics runnerConfig handlers
```

`jobRunDiagnosticMessage` renders the same structured message used by the
stdout logger.

Pure helpers are available for tests and custom runners:

- `newJob`
- `enqueueJobCommand`
- `enqueueIdempotentJobCommand`
- `claimJob`
- `jobIsDead`
- `markDeadJob`
- `jobIsStale`
- `jobRetryDelaySeconds`
- `recoverStaleJob`
- `markDeadJobsCommand`
- `timeoutRunningJobsCommand`
- `newJobWorkerControl`
- `requestJobWorkerStop`
- `runJobRunnerOnceWithDiagnostics`
- `runJobRunnerLoopWithControl`
- `startJobRunner`
- `succeedJob`
- `failJob`

Use `queryJobDashboard` to load real queue state, then render it behind an
authenticated admin route with `jobDashboard`.

## 3. Scripts

A script is an app-context function:

```idris
myScript : Script
myScript context =
  case requireScriptArg "email" 0 context of
    Left error => do
      putStrLn error
      pure 1
    Right email => do
      putStrLn ("processing " ++ email)
      pure 0
```

Run it:

```idris
main : IO ()
main =
  runScript config args env myScript
```

`ScriptContext` contains:

- `AppConfig`
- derived `Database`
- positional args
- explicit environment values

Helpers:

- `scriptContext`
- `scriptArg`
- `scriptEnv`
- `requireScriptArg`
- `requireScriptEnv`
- `runScript`

## 4. Typed Mail

Render mail from typed input:

```idris
record WelcomeInput where
  constructor MkWelcomeInput
  email : String
  username : String

welcomeMail : TypedMail WelcomeInput
welcomeMail =
  typedMail (\input =>
    MkMailMessage
      (mailAddress "hello@example.com")
      [mailAddress input.email]
      []
      []
      ("Welcome, " ++ input.username)
      "Welcome to the app"
      (Just (<p>Welcome to the app.</p>))
      []
      [])
```

Render and deliver:

```idris
message : MailMessage
message =
  renderMail welcomeMail input

result : IO (Either String MailDelivery)
result =
  deliverMail transport message
```

Development tools:

- `devMailTransport`
- `captureMail`
- `mailPreview`
- `mailboxPreview`
- `devMailboxRoutes`
- `mailDeliveryDiagnostics`

Production delivery is app-owned through `MailTransport`, so SMTP, API-based
mail services, and queue-backed delivery can all fit behind the same boundary.

Expose a development mailbox behind your dev-only route list:

```idris
devRoutes : DevMailbox -> Routes
devRoutes mailbox =
  devMailboxRoutes "/dev/mail" mailbox
```

The generated routes serve a mailbox index at `/dev/mail` and per-message
previews such as `/dev/mail/1`. Mount them only in development; the mailbox is
an in-memory preview surface, not a production delivery store.

## 5. MIME And Attachments

Attach in-memory files to a typed message:

```idris
messageWithAttachment : MailMessage
messageWithAttachment =
  { attachments := [mailAttachment "receipt.txt" "text/plain" "Thanks"] } message
```

Render a multipart MIME message for transports that expect full message text:

```idris
rawMessage : String
rawMessage =
  renderMailMime "iwf-mail-boundary" messageWithAttachment
```

`renderMailMime` emits `multipart/alternative` for text plus HTML bodies and
wraps that in `multipart/mixed` when attachments are present. Attachment content
is carried as provided; applications that need base64, DKIM signing, or provider
specific upload APIs should perform that in their `MailTransport`.

Delivery diagnostics are pure and transport-neutral:

```idris
diagnostics : MailDeliveryDiagnostics
diagnostics =
  mailDeliveryDiagnostics message delivery
```

Diagnostics include delivery status, transport name on success, recipient count,
attachment count, whether an HTML body was present, and the transport error on
failure.

## 6. Mail Transport Boundary

Iwf does not ship a production sendmail or SMTP transport. Applications provide
the `MailTransport` function they want to run in production, then call
`deliverMail transport message`.

For sendmail-style delivery, Iwf exposes only the command shape:

```idris
sendmailCommand "/usr/sbin/sendmail"
```

That returns `["/usr/sbin/sendmail", "-t", "-i"]`. The app remains responsible
for process execution, timeouts, exit-code handling, logging, retries, and
where delivery runs.

For SMTP delivery, keep the SMTP client, credentials, TLS policy, retries, and
provider-specific error handling in the app transport. Iwf's boundary is the
typed `MailMessage` and the `MailTransport` result contract, not a built-in SMTP
client.

## Next

Read [File Uploads and Storage](file-uploads-and-storage.md).
