FreeReportv0.13.0한국어

Scheduled reports (@freereportjs/server)

@freereportjs/server renders reports on a schedule (daily to cron, any time zone) and e-mails, files or prints them, per recipient, with retries.

@freereportjs/server turns server rendering into scheduled delivery: it runs report jobs at set times in their time zone, renders PDF, Excel and Word without a browser, and e-mails (SMTP through nodemailer), files, posts or prints them. A job with recipients renders once per recipient with that person's parameters, so each branch manager gets only their own branch. Data loading and every delivery are retried with backoff, and each run appends one line to a JSONL audit log. Node 18+; nodemailer is needed only for e-mail.

import { createScheduler } from '@freereportjs/server';

const scheduler = createScheduler({
  timezone: 'Asia/Seoul',
  fonts: { 'Nanum Gothic': { normal: 'fonts/NanumGothic.ttf', bold: 'fonts/NanumGothicBold.ttf' } },
  outDir: 'out',                            // files, and out/audit.jsonl
  smtp: process.env.SMTP_URL,               // smtps://user:pass@smtp.example.com:465
  jobs: [{
    name: 'branch-sales',
    schedule: 'monthly 1 09:00',            // or 'weekly mon 09:00', '0 9 * * 1-5'
    report: 'reports/branch-sales.report.json',
    data: 'https://erp.example.com/api/sales?branch={params.branch}&from={params.from}&to={params.to}',
    params: (runDate, { tokens }) => ({ from: tokens.prevMonthStart, to: tokens.prevMonthEnd, month: tokens.prevYearMonth }),
    outputs: ['pdf', 'xlsx'],
    fileName: '지점매출-{params.month}',
    recipients: [                           // one render each, with their own params
      { to: 'seoul@example.com', name: '서울', params: { branch: '서울' } },
      { to: 'busan@example.com', name: '부산', params: { branch: '부산' } },
    ],
    deliver: [
      { type: 'email', cc: 'hq@example.com', subject: '[{params.month}] {recipient.name} 지점 매출', body: '{recipient.name} 지점장님, 지난달 보고서입니다.' },
      { type: 'folder', dir: 'archive/{yearMonth}' },
    ],
  }],
});
scheduler.start();
await scheduler.runNow('branch-sales', { dryRun: true });   // files + .eml, nothing sent
FunctionReturnsDescription
createScheduler({ jobs, fonts, timezone?, outDir?, auditLog?, smtp?, retry?, log?, onRun? })SchedulerChecks the jobs and returns a scheduler. fonts: TTF bytes, paths or URLs by family (default Nanum Gothic from jsDelivr). auditLog defaults to <outDir>/audit.jsonl.
scheduler.start() / stop()void / PromiseArm a timer per enabled job; stop waits for runs in progress. Runs of one job never overlap.
scheduler.runNow(name, { now?, dryRun? })Promise<AuditRecord>Run one job now, also a disabled one.
scheduler.next(after?){ name, timezone, next }[]The next run of each enabled job.
runJob(job, { now?, dryRun?, ...options })Promise<AuditRecord>Run a job once without a scheduler. Never throws for a failed run: status is ok, partial or error.
parseSchedule(text, tz?) / nextRun(s, after?) / dateTokens(date, tz)…The schedule parser, next-run calculator and the date tokens used in params and templates.

Jobs

NameTypeDescription
schedulestring"daily 09:00", "weekdays 08:30", "weekly mon,thu 09:00", "monthly 1 09:00" (29–31: last day of shorter months), "monthly last 18:00", or 5-field cron "0 9 * * 1-5" (lists, ranges, steps, names, L = last day). An IANA zone may follow. DST: a time in the spring gap runs an hour later; a repeated autumn time runs once.
timezonestringThe job's zone (default: the scheduler's, then the machine's).
reportdefinition | pathThe report definition or a path to its JSON.
dataurl | REST | { file } | { module } | (ctx) => dataWhere the rows come from. REST definitions are the same as the data of a definition ({params.x} filled and escaped). Default: the data of the definition itself.
paramsobject | (runDate, ctx) => objectParameters; strings may use date tokens such as {prevMonthStart}, or compute them from the run date (e.g. last month).
outputs('pdf' | 'xlsx' | 'xlsx-data' | 'docx')[]Files to make (default ['pdf']).
recipients{ to, cc?, name?, params }[]One render per recipient with their own params, e.g. each branch manager gets their own branch. E-mail goes to that recipient.
delivertarget[]email (to, cc, subject, body, smtp), folder (dir, filename), webhook (url), put (url), printAgent (token, printer). Templates: {yesterday}, {params.x}, {recipient.name}, {title} …
retry{ retries, delayMs, factor }Data loading and each target are retried with backoff (default 2 retries, 30 s, doubling).
fileName / title / locale / pdf / enabled…File name and subject templates, number/date locale, PDF options (encryption), false = not scheduled.

Date tokens are in the job's zone: {today} {yesterday} {weekAgo} {monthStart} {monthEnd} {prevMonthStart} {prevMonthEnd} {yearMonth} {prevYearMonth} {year} {now}. A dry run renders and writes every file, writes each mail as an .eml file with its attachments, and skips webhooks, uploads and printing.

Command line

jobs.json holds the same options and jobs as JSON, with paths relative to the file. Secrets stay in environment variables: write ${env:SMTP_PASS} and it is filled when the job runs, so a missing variable fails only that job. The reports.json of the scheduled-reports example works as is.

npm i @freereportjs/core @freereportjs/server nodemailer   # nodemailer only for e-mail

npx freereport-server validate --config jobs.json        # schedules, targets, checkReport
npx freereport-server next --config jobs.json --count 3  # next run times
npx freereport-server run --config jobs.json --now branch-sales --dry-run
npx freereport-server run --config jobs.json             # keep running (NSSM, systemd)
CommandUseDescription
freereport-server run --config jobs.jsonserviceKeep running; each job runs on its schedule (SIGINT / SIGTERM stops).
run --now <name> [--dry-run]onceRun one job (or --now all) and exit with 1 when it failed. --dry-run writes files and .eml mails and sends nothing.
validate --config jobs.jsoncheckSchedules, outputs, targets, environment variables, and checkReport on every report.
next --config jobs.json [--count n]listThe next run times of each job in its zone.
pair-agentWindowsApprove this service in the FreeReport Windows print program once and print its token.

Audit log

One JSON line per run, failures included. status is ok, partial (some recipients or targets failed) or error. docHash is the SHA-256 of the printed text (the originality fingerprint, the same when the same content is rendered again); sha256 hashes the exact file that was sent. Secrets and signed URL query strings are never written.

{"time":"2026-11-01T00:00:04.120Z","job":"branch-sales","schedule":"monthly 1 09:00","runAt":"2026-11-01T00:00:00.000Z",
 "dryRun":false,"status":"ok","recipients":["seoul@example.com","hq@example.com","busan@example.com"],"size":182344,"durationMs":4120,
 "renders":[{"recipient":"서울","params":{"branch":"서울"},"pages":3,"rows":120,"docHash":"5c1e…",
   "files":[{"name":"지점매출-2026-10-서울.pdf","format":"pdf","size":130484,"sha256":"815f…"}],
   "deliveries":[{"type":"email","to":["seoul@example.com"],"cc":["hq@example.com"],"messageId":"<…>"}]}, …]}

Run it as a Windows service (NSSM), from Task Scheduler with run --now, or under systemd; the scheduled-reports example shows each, plus a Cloudflare Workers Cron variant. Printing on the Windows print program works only on the PC where that program runs, while its user is logged on. Download the example (zip)