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"
}argsuses the entity field grammar (varchar, int, jsonb, arrays).visible_to "directory"restricts which caller may submit the job and claim it.
2. Apply
tide applyThe declaration is additive. The server enforces retries, timeout, and
queue at submit and claim time.
3. Generate, implement, and register the handler
tide generateAlongside the entity clients, tide generate writes a jobs module per
namespace — client/<ns>/jobs.go for Go, atlantis/<ns>/v1/jobs.py for
Python — with a typed args type, a handler type, 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})from atlantis.directory.v1 import ImportContactsArgs, register_import_contacts
from atlantis_client.jobs import Registry, checkpoint
def handle(args: ImportContactsArgs) -> None:
checkpoint(10, "fetching contacts")
# ... your import logic, reading args.account_id ...
registry = Registry()
register_import_contacts(registry, handle)Python raises rather than returning an error, and the args arrive as a
frozen dataclass built from the row’s JSON. A timestamptz arg is an
RFC 3339 str there, not a datetime — job args follow the JSON
projection, which the type mapping and
atlantis_client.jobs.argjson cover.
The helper decodes the row’s args JSON into the typed value before invoking
your handler. (Registering under the raw id "directory.ImportContacts"
and decoding yourself 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.
from atlantis_client import connect_from_store
from atlantis_client.jobs import DispatchedWorker, WorkerConfig
with connect_from_store("acme", "directory") as channel:
DispatchedWorker(channel, registry, WorkerConfig(queue="contacts")).run()run() blocks; stop() from another thread ends it. Handlers run on a
thread pool sized by WorkerConfig.max_in_flight.
The queue must be one a declared job runs on ("default" when no job
names one) — an unknown queue is refused at session open. Both workers
reconnect 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 0198f2c1a4e07000Services 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. The procedure’s RPC answers from the next
request after tide apply — see
Custom queries and procedures.
Verify
job-id 0198f2c1a4e07000
job-name directory.ImportContacts
queue contacts
status complete
attempts 1 / 3
enqueued 2026-09-02T14:03:11ZThe status reaches complete, with any checkpoints your handler reported.
The console’s Workers page shows the connected worker session and its
queue.
Next steps
- Operate jobs and workflows — monitoring, the dead-letter queue, retries.
- Write a long-running handler — idempotency, leases, and resume-from-progress in depth.
- Jobs and workflows — the model.