Go SDK
Installation
Section titled “Installation”go get chronos.sh/go- Requires Go 1.26 or later
- Standard library only
- There is no pull worker yet. Use push delivery and verify each delivery with
Webhook.Verify, or claim pull jobs with the Node.js SDK worker.
Every exported type, field and constant is documented on pkg.go.dev.
Client
Section titled “Client”import chronos "chronos.sh/go"
client, err := chronos.NewClient(os.Getenv("CHRONOS_API_KEY"))if err != nil { return err}The import needs the chronos name because the path ends in go. NewClient rejects an empty key; the API checks the key on the first request. To check it up front, call client.Account.Get(ctx), which returns the workspace the key belongs to. A Client is safe for concurrent use, so create one and share it.
| Option | Default | Effect |
|---|---|---|
WithBaseURL(url) |
https://api.chronos.sh |
Sends requests to another Chronos API, such as a local one. Must be an absolute http or https URL. |
WithHTTPClient(c) |
A new http.Client |
Sends requests with your client. It is copied, and the copy never follows redirects. Its Timeout, if set, also bounds each attempt. |
WithRequestTimeout(d) |
30s |
Bounds each attempt, including reading the response. |
WithMaxRequestRetries(n) |
2 |
Sets the retries for requests that are safe to repeat. 0 turns retries off. See Retries. |
WithUserAgent(token) |
none | Adds a product token, such as my-app/1.2.0, ahead of chronos-go/<version>. |
The examples below continue from this block: client and err exist, and ctx is your context.Context.
Resources
Section titled “Resources”The client has one field per resource. Every method takes a context.Context first. Request and response fields use the API’s snake_case names in JSON, and the REST reference documents every field.
client.Schedules
Section titled “client.Schedules”| Method | Request | Returns |
|---|---|---|
Create(ctx, params) |
POST /v1/schedules |
*Schedule |
List(ctx, params) |
GET /v1/schedules |
*Page[*Schedule] |
Iterate(ctx, params) |
GET /v1/schedules, every page |
iter.Seq2[*Schedule, error] |
Get(ctx, id) |
GET /v1/schedules/{id} |
*Schedule |
Update(ctx, id, params) |
PATCH /v1/schedules/{id} |
*Schedule |
Delete(ctx, id) |
DELETE /v1/schedules/{id} |
error only |
Pause(ctx, id) |
POST /v1/schedules/{id}/pause |
*Schedule |
Resume(ctx, id) |
POST /v1/schedules/{id}/resume |
*Schedule |
Trigger(ctx, id) |
POST /v1/schedules/{id}/trigger |
*Job |
Archive(ctx, id) |
POST /v1/schedules/{id}/archive |
*Schedule |
Param types: ScheduleCreateParams, ScheduleUpdateParams, ScheduleListParams.
client.Jobs
Section titled “client.Jobs”| Method | Request | Returns |
|---|---|---|
Create(ctx, params) |
POST /v1/jobs |
*Job |
List(ctx, params) |
GET /v1/jobs |
*Page[*Job] |
Iterate(ctx, params) |
GET /v1/jobs, every page |
iter.Seq2[*Job, error] |
Get(ctx, id) |
GET /v1/jobs/{id} |
*Job |
Cancel(ctx, id) |
POST /v1/jobs/{id}/cancel |
*Job |
Skip(ctx, id) |
POST /v1/jobs/{id}/skip |
*Job |
Retry(ctx, id) |
POST /v1/jobs/{id}/retry |
*Job |
Wait(ctx, id, opts) |
GET /v1/jobs/{id}, polled |
*Job |
Param types: JobCreateParams, JobListParams, JobWaitOptions.
Jobs.Create is never retried after a 5xx or a network error, because the job may already exist. To retry it yourself, set an IdempotencyKey and treat ErrorCodeIdempotencyKeyConflict as success:
_, err = client.Jobs.Create(ctx, chronos.JobCreateParams{ Name: "Charge invoice inv_789", Handler: new("charge-invoice"), IdempotencyKey: new("charge-inv_789"),})// The job already exists, so a retried create can treat this as success.if apiErr, ok := errors.AsType[*chronos.APIError](err); ok && apiErr.Code == chronos.ErrorCodeIdempotencyKeyConflict { err = nil}if err != nil { return err}Jobs.Wait polls the job until it is completed, failed, cancelled or skipped, and returns it. It polls every second unless you set JobWaitOptions.PollInterval. A failed job is returned, not an error, so check its Status. Wait stops at the deadline on ctx, or after 60 seconds if ctx has none, and returns a *WaitTimeoutError holding the last state it saw (nil if no request finished):
job, err := client.Jobs.Wait(ctx, jobID, chronos.JobWaitOptions{})if err != nil { return err}fmt.Println(job.Status)client.Executions
Section titled “client.Executions”| Method | Request | Returns |
|---|---|---|
List(ctx, params) |
GET /v1/executions |
*Page[*Execution] |
Iterate(ctx, params) |
GET /v1/executions, every page |
iter.Seq2[*Execution, error] |
Get(ctx, id) |
GET /v1/executions/{id} |
*Execution |
Param type: ExecutionListParams. Set ScheduleID: chronos.Null[string]() to match executions of jobs without a schedule.
client.Account
Section titled “client.Account”| Method | Request | Returns |
|---|---|---|
Get(ctx) |
GET /v1/account |
*Account |
Optional and nullable fields
Section titled “Optional and nullable fields”Optional request fields are pointers, and nil leaves the field out. Since Go 1.26, new(value) returns a pointer to a copy of the value:
_, err = client.Schedules.Create(ctx, chronos.ScheduleCreateParams{ Name: "Nightly report", Cron: new("0 2 * * *"), Timezone: new("Europe/London"), MaxRetries: new(0), // sent as 0, not left out Delivery: &chronos.Delivery{ Type: chronos.DeliveryTypePush, HTTP: &chronos.HTTPDelivery{URL: "https://your-app.com/hooks/chronos", Method: "POST"}, },})if err != nil { return err}ScheduleUpdateParams can also clear Description, Handler, Runs, StopAt and Payload. Those fields use chronos.Nullable. Its zero value leaves the field out, chronos.NewNullable(v) sets it, and chronos.Null[T]() sends null:
_, err = client.Schedules.Update(ctx, scheduleID, chronos.ScheduleUpdateParams{ Name: new("Nightly report (EU)"), Runs: chronos.Null[int](), // remove the run limit})if err != nil { return err}Numbers and timestamps
Section titled “Numbers and timestamps”Timestamps are time.Time, and nullable response fields are pointers. Numbers inside Payload and Result decode as json.Number, with the exact digits the API sent. The API reads numbers as 64-bit floats, so send integers above 2^53 as strings.
Lists and pages
Section titled “Lists and pages”List returns one page, newest first, with Data and Paging:
page, err := client.Jobs.List(ctx, chronos.JobListParams{Status: chronos.JobStatusFailed, Page: 2, Size: 50})if err != nil { return err}fmt.Println(len(page.Data), page.Paging.Total)Iterate walks every page, fetching 100 items per request unless you set Size. Each item is yielded at most once, and breaking out of the loop stops further requests. Pages are offsets, so an item deleted or changed out of your filter during the walk shifts later items onto earlier pages, and Iterate can miss some of them.
That includes changes you make yourself. Retrying a failed job moves it out of JobStatusFailed, so collect the ids before acting on them:
var failed []stringfor job, err := range client.Jobs.Iterate(ctx, chronos.JobListParams{Status: chronos.JobStatusFailed}) { if err != nil { return err } failed = append(failed, job.ID)}for _, id := range failed { if _, err := client.Jobs.Retry(ctx, id); err != nil { return err }}Retries
Section titled “Retries”The client retries a request only when repeating it is safe:
| Failure | GET | POST, PATCH, DELETE |
|---|---|---|
| 429 | Retried | Retried |
| 5xx | Retried | Returned |
| Network error or timeout | Retried | Returned |
| Other 4xx | Returned | Returned |
A 429 is safe to retry for any method, because the API rejects it before doing any work.
- Waits back off from 0.5s, doubling up to 8s, each shortened by up to 25% of random jitter. A
Retry-Afterheader sets the wait instead. Above 60 seconds, the client returns the error without waiting. - A retry that would outlast the context’s deadline is skipped, and the last error is returned.
WithRequestTimeoutbounds each attempt. A deadline onctxbounds all attempts together.- Redirects are never followed, so a 3xx comes back as an
*APIError.
Errors
Section titled “Errors”| Error | When | Check with |
|---|---|---|
*APIError |
The API answered with a non-2xx status or "success": false |
errors.AsType[*chronos.APIError](err) |
ErrUnauthorized, ErrNotFound, ErrRateLimited |
An *APIError with status 401, 404 or 429 |
errors.Is(err, chronos.ErrNotFound) |
*NetworkError |
No usable response: DNS failure, refused or reset connection, timeout | errors.AsType[*chronos.NetworkError](err) |
ErrTimeout |
A *NetworkError for an attempt that timed out |
errors.Is(err, chronos.ErrTimeout) |
ErrInvalidResponse |
A 2xx response without the documented shape | errors.Is(err, chronos.ErrInvalidResponse) |
ErrInvalidID |
An empty id, . or .., rejected before any request |
errors.Is(err, chronos.ErrInvalidID) |
*WaitTimeoutError |
Jobs.Wait reached its deadline |
errors.AsType[*chronos.WaitTimeoutError](err) |
*APIError carries StatusCode, Code, Message, RequestID, RetryAfter, RateLimit, FieldErrors and the raw Body. Branch on Code, using the ErrorCode constants. The API can add codes, so keep a default branch. See Error Codes for what each code means.
_, err = client.Schedules.Pause(ctx, scheduleID)if apiErr, ok := errors.AsType[*chronos.APIError](err); ok && apiErr.Code == chronos.ErrorCodeScheduleAlreadyPaused { err = nil // already paused}if err != nil { return err}An ErrTimeout error also matches context.DeadlineExceeded. Cancelling your own context, or reaching its deadline, returns ctx.Err() instead, so check ctx.Err() to tell the two apart. Jobs.Wait is the exception. At a deadline it returns a *WaitTimeoutError, which also matches context.DeadlineExceeded.
Webhooks
Section titled “Webhooks”Webhook.Verify checks the signature on push deliveries. Create the verifier with NewWebhook; it needs no client or API key. Pass every active signing secret during a rotation, and a delivery signed with any of them is accepted.
webhook, err := chronos.NewWebhook([]string{os.Getenv("CHRONOS_SIGNING_SECRET")})if err != nil { log.Fatal(err)}
http.HandleFunc("/hooks/chronos", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) if err != nil { http.Error(w, "unreadable body", http.StatusBadRequest) return } if err := webhook.Verify(body, r.Header); err != nil { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // Decode body and run the job.})Verify checks the X-Chronos-Signature, X-Chronos-Timestamp and X-Chronos-Delivery-Id headers against the raw body. Every failure wraps ErrInvalidWebhook. A timestamp more than 5 minutes from now is rejected. To change the window, pass WithTimestampTolerance(d) to NewWebhook; 0 turns the check off and removes replay protection.
See Signature Verification for the signing scheme.