Iwf Guide
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:
schemaSql : String
schemaSql = jobQueueSchemaSql
Database operations are exposed as SQL helpers:
enqueueJobSqlenqueueIdempotentJobSqlclaimNextJobSqlsucceedJobSqlretryJobSqlfailJobSqlmarkDeadJobsSqltimeoutRunningJobsSqljobDashboardQuerySqldeadJobsQuerySql
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:
sendWelcomeEmail : JobPerformer
sendWelcomeEmail job =
-- decode job.payload and send mail
pure (Right ())
handlers : List JobHandler
handlers =
[ jobHandler "send-welcome-email" sendWelcomeEmail ]
Run one claim:
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:
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:
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:
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:
newJobenqueueJobCommandenqueueIdempotentJobCommandclaimJobjobIsDeadmarkDeadJobjobIsStalejobRetryDelaySecondsrecoverStaleJobmarkDeadJobsCommandtimeoutRunningJobsCommandnewJobWorkerControlrequestJobWorkerStoprunJobRunnerOnceWithDiagnosticsrunJobRunnerLoopWithControlstartJobRunnersucceedJobfailJob
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:
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:
main : IO ()
main =
runScript config args env myScript
ScriptContext contains:
AppConfig- derived
Database - positional args
- explicit environment values
Helpers:
scriptContextscriptArgscriptEnvrequireScriptArgrequireScriptEnvrunScript
4. Typed Mail
Render mail from typed input:
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:
message : MailMessage
message =
renderMail welcomeMail input
result : IO (Either String MailDelivery)
result =
deliverMail transport message
Development tools:
devMailTransportcaptureMailmailPreviewmailboxPreviewdevMailboxRoutesmailDeliveryDiagnostics
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:
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:
messageWithAttachment : MailMessage
messageWithAttachment =
{ attachments := [mailAttachment "receipt.txt" "text/plain" "Thanks"] } message
Render a multipart MIME message for transports that expect full message text:
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:
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:
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.