Skip to content

Declare a background job

Declare a typed job in .atl, implement its handler in your own service, and run the submit-claim-complete loop.

Declare a typed job in .atl, implement its handler in your own service, and run the submit–claim–complete loop.

1. Declare the job

In your repository’s .atl:

job ImportContacts in directory {
  args {
    account_id      varchar(64) not null
    import_strategy varchar(20) not null default "skip"
  }
  retries  3
  timeout  30m
  queue    "contacts"
  visible_to "directory"
}
  • args uses the entity field grammar (varchar, int, jsonb, arrays).
  • visible_to "directory" restricts which caller may submit the job and claim it.

2. Apply

tide apply

The declaration is additive. The server enforces retries, timeout, and queue at submit and claim time.

3. Generate, implement, and register the handler

tide generate

Alongside the entity clients, tide generate writes client/<ns>/jobs.go with a typed Args struct, a handler interface, and a Register<Job> helper per declared job. Handlers run in your service’s binary:

type importContactsHandler struct{ crm *crm.Client }

func (h *importContactsHandler) Handle(ctx context.Context, args directory.ImportContactsArgs) error {
    jobs.Checkpoint(ctx, 10, "fetching contacts")
    // ... your import logic, reading args.AccountId ...
    return nil
}

registry := jobs.NewRegistry()
directory.RegisterImportContacts(registry, &importContactsHandler{crm: crmClient})

The helper decodes the row’s args JSON into the typed struct before invoking Handle. (Registering under the raw id "directory.ImportContacts" with jobs.HandlerFunc and your own json.Unmarshal works the same way.)

A non-nil return retries the attempt up to the declared retries, then dead-letters it. jobs.Checkpoint(ctx, pct, msg) reports progress and extends the attempt’s lease; long-running handlers call it as they go.

4. Run a worker

The worker holds a gRPC session to your organisation’s server and receives dispatched work over it — your service needs no database access:

w := jobs.NewDispatchedWorker(conn, registry, "contacts", jobs.ServerConfig{
    Logger: slog.Default(),
})
go w.Run(ctx)

conn is your service’s authenticated *grpc.ClientConn to atlantis. The queue must be one a declared job runs on ("default" when no job names one) — an unknown queue is refused at session open. Run reconnects on stream errors with backoff; work in flight when a worker dies is re-dispatched to another worker after its lease expires, so handlers must be idempotent.

5. Submit

tide job submit directory.ImportContacts \
  --args='{"account_id":"acct_123","import_strategy":"replace"}'
 submitted directory.ImportContacts as job 0198f2c1a4e07000
       monitor with: tide job status 0198f2c1a4e07000

Services submit through the same gRPC API (SubmitJob, requiring the JOBS_WRITE capability), or atomically from a procedure:

procedure ConnectAccount for directory.Account {
  input { account_id: varchar(64) }
  steps {
    update Account set status = "connected" where account_id = $account_id
    enqueue directory.ImportContacts(account_id: $account_id, import_strategy: "replace")
  }
}

The enqueue shares the procedure’s transaction: if the procedure rolls back, the job is never enqueued. A brand-new procedure’s RPC becomes callable at the next server restart — see Custom queries and procedures.

Verify

tide job status 0198f2c1a4e07000tideout
job-id     0198f2c1a4e07000
job-name   directory.ImportContacts
queue      contacts
status     complete
attempts   1 / 3
enqueued   2026-09-02T14:03:11Z

The status reaches complete, with any checkpoints your handler reported. The console’s Workers page shows the connected worker session and its queue.

Next steps

Navigation

Type to search…

↑↓ navigate↵ selectEsc close