Gonvex

Scheduling and Background Jobs

Register recurring jobs and enqueue mutations or actions for later execution.

Scheduling and Background Jobs

Gonvex can run recurring project jobs and enqueue one-shot work from a backend function. Scheduled calls use the same mutation/action execution paths, project/tenant routing, metrics, and realtime invalidation as client calls.

Recurring jobs

Register a mutation or action by interval:

package backend

import (
  "time"

  "github.com/gonvex/gonvex/pkg/gonvex"
)

func Register(app *gonvex.App) {
  app.Action("reports.refresh", RefreshReports)
  app.Cron(
    "refresh reports",
    15*time.Minute,
    "reports.refresh",
    RefreshReportsArgs{},
  )
}

Or use a standard five-field cron expression:

app.CronExpr(
  "nightly cleanup",
  "0 3 * * *",
  "maintenance.cleanup",
  CleanupArgs{},
)

The five fields are minute, hour, day of month, month, and day of week. The referenced function must be a mutation, internal mutation, or action, and the arguments must be JSON-encodable.

Cron names must be unique within an app. Manifest sync replaces the project's registered schedules while preserving run statistics for unchanged jobs.

Per-tenant cron jobs

Use a tenant cron when each provisioned tenant should receive its own invocation:

app.TenantCron(
  "expire stale sessions",
  time.Hour,
  "sessions.expire",
  ExpireSessionsArgs{},
)

app.TenantCronExpr(
  "tenant daily summary",
  "30 6 * * *",
  "summaries.create",
  CreateSummaryArgs{},
)

Each invocation has that tenant's ctx.TenantID and database route. A normal Cron/CronExpr is project-scoped and has no tenant ID.

One-shot jobs

Every runtime context exposes ctx.Scheduler. Enqueue work after a delay:

func CreateTask(
  ctx *gonvex.MutationCtx,
  args CreateTaskArgs,
) (Task, error) {
  task, err := insertTask(ctx, args)
  if err != nil {
    return Task{}, err
  }

  _, err = ctx.Scheduler.RunAfter(
    10*time.Minute,
    "tasks.sendReminder",
    SendReminderArgs{TaskID: task.ID},
  )
  return task, err
}

Or run at an exact time:

jobID, err := ctx.Scheduler.RunAt(
  args.PublishAt,
  "posts.publish",
  PublishPostArgs{PostID: args.PostID},
)

Jobs scheduled inside a mutation are held until that mutation commits. A failed or rolled-back mutation does not expose its queued work. The new call inherits the current project and tenant scope.

Realtime and observability

Successful scheduled mutations and actions invalidate live queries using their declared gonvex.Writes(...) dependencies. A long-running action that commits work before returning can notify subscribers immediately:

ctx.NotifyTableChanged("tasks", "task_events")

The dashboard Health view reports registered cron schedules, recent runs, failures, scheduler lag, execution duration, queued work, and concurrency. Scheduled function calls also appear in normal function metrics.

Durability and multi-replica execution

Every one-shot job and deterministic cron occurrence is stored in the control Postgres database before it can be claimed. A runtime restart or rolling replacement therefore does not discard scheduled work. Replicas claim due rows with PostgreSQL row locks, renewable leases, and fencing tokens, while one-shot jobs are always selected ahead of an overdue cron backlog.

Recurring definitions still come from the compiled app and are rebuilt after runtime startup or manifest sync. Their occurrence IDs are deterministic, so overlapping replicas can derive the same fire without executing it twice. One runtime process executes up to 16 scheduled calls concurrently.

The runtime retains completed rows for deduplication and auditability. A normal function error is a terminal scheduled result; only an interrupted execution such as runtime shutdown or lease loss is released for recovery. There is not yet a public cancellation, retry, or dead-letter API.

On this page