curl --request POST \
--url https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"schedule": "0 9 * * 1-5",
"timezone": "America/New_York",
"command": "python /app/report.py"
}
'import requests
url = "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron"
payload = {
"schedule": "0 9 * * 1-5",
"timezone": "America/New_York",
"command": "python /app/report.py"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
schedule: '0 9 * * 1-5',
timezone: 'America/New_York',
command: 'python /app/report.py'
})
};
fetch('https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'schedule' => '0 9 * * 1-5',
'timezone' => 'America/New_York',
'command' => 'python /app/report.py'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron"
payload := strings.NewReader("{\n \"schedule\": \"0 9 * * 1-5\",\n \"timezone\": \"America/New_York\",\n \"command\": \"python /app/report.py\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"schedule\": \"0 9 * * 1-5\",\n \"timezone\": \"America/New_York\",\n \"command\": \"python /app/report.py\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"schedule\": \"0 9 * * 1-5\",\n \"timezone\": \"America/New_York\",\n \"command\": \"python /app/report.py\"\n}"
response = http.request(request)
puts response.read_body{
"id": "cron_3f2a9c1e-7b4d-4e8a-9c1f-2d3e4f5a6b7c",
"sailbox_id": "sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d",
"schedule": "0 9 * * 1-5",
"timezone": "America/New_York",
"command": "python /app/report.py",
"next_run_at": "2030-01-01T14:00:00Z",
"created_at": "2029-12-31T18:30:00Z"
}{
"error": {
"message": "memory_limit_gib for size m must be between 8 and 128",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}{
"error": {
"message": "Your API key has been disabled due to insufficient credits. Visit https://app.sailresearch.com/billing to add credits.",
"type": "billing_error",
"param": null,
"code": "credits_exhausted",
"billing_url": "https://app.sailresearch.com/billing"
}
}{
"error": {
"message": "sailboxes require an organization-scoped API key",
"type": "permission_error",
"param": null,
"code": null
}
}{
"error": {
"message": "sailbox \"sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d\" not found",
"type": "not_found_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotency key reused with a different request body",
"type": "conflict_error",
"param": null,
"code": null
}
}{
"error": {
"message": "request body too large",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Too many concurrent requests. Please retry after some of your organization's in-flight requests complete.",
"type": "rate_limit_error",
"param": null,
"code": "rate_limited"
}
}{
"error": {
"message": "failed to fetch sailbox",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Authentication service unavailable",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotent request still in flight",
"type": "server_error",
"param": null,
"code": null
}
}Add a cron job
Adds a job that runs a shell command in the Sailbox on a schedule. When a run is due and the Sailbox is asleep, Sail wakes it and starts the command. See Run a command on a schedule for how runs behave.
curl --request POST \
--url https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"schedule": "0 9 * * 1-5",
"timezone": "America/New_York",
"command": "python /app/report.py"
}
'import requests
url = "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron"
payload = {
"schedule": "0 9 * * 1-5",
"timezone": "America/New_York",
"command": "python /app/report.py"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
schedule: '0 9 * * 1-5',
timezone: 'America/New_York',
command: 'python /app/report.py'
})
};
fetch('https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'schedule' => '0 9 * * 1-5',
'timezone' => 'America/New_York',
'command' => 'python /app/report.py'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron"
payload := strings.NewReader("{\n \"schedule\": \"0 9 * * 1-5\",\n \"timezone\": \"America/New_York\",\n \"command\": \"python /app/report.py\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"schedule\": \"0 9 * * 1-5\",\n \"timezone\": \"America/New_York\",\n \"command\": \"python /app/report.py\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/cron")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"schedule\": \"0 9 * * 1-5\",\n \"timezone\": \"America/New_York\",\n \"command\": \"python /app/report.py\"\n}"
response = http.request(request)
puts response.read_body{
"id": "cron_3f2a9c1e-7b4d-4e8a-9c1f-2d3e4f5a6b7c",
"sailbox_id": "sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d",
"schedule": "0 9 * * 1-5",
"timezone": "America/New_York",
"command": "python /app/report.py",
"next_run_at": "2030-01-01T14:00:00Z",
"created_at": "2029-12-31T18:30:00Z"
}{
"error": {
"message": "memory_limit_gib for size m must be between 8 and 128",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}{
"error": {
"message": "Your API key has been disabled due to insufficient credits. Visit https://app.sailresearch.com/billing to add credits.",
"type": "billing_error",
"param": null,
"code": "credits_exhausted",
"billing_url": "https://app.sailresearch.com/billing"
}
}{
"error": {
"message": "sailboxes require an organization-scoped API key",
"type": "permission_error",
"param": null,
"code": null
}
}{
"error": {
"message": "sailbox \"sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d\" not found",
"type": "not_found_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotency key reused with a different request body",
"type": "conflict_error",
"param": null,
"code": null
}
}{
"error": {
"message": "request body too large",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Too many concurrent requests. Please retry after some of your organization's in-flight requests complete.",
"type": "rate_limit_error",
"param": null,
"code": "rate_limited"
}
}{
"error": {
"message": "failed to fetch sailbox",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Authentication service unavailable",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotent request still in flight",
"type": "server_error",
"param": null,
"code": null
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
Makes the request retry-safe. Sail remembers the answer it sent under a key, including a 400 or a 409, and replays it with Idempotent-Replayed: true for a retry that repeats the key, the method, the path, and the body. Bodies are compared byte for byte. Reusing a key for a different request returns 409, so send a corrected request under a new key. A key that is blank or only spaces is ignored, and the request runs without idempotency.
Any string of up to 255 bytes works, and characters outside ASCII count for more than one. A UUID is a good default. Keys are remembered for at least 24 hours and scoped to the API key that sent them.
A server error, or a failure to record the answer, can leave a key unsettled, and creating a Sailbox is where that matters. See Retrying safely.
255Path Parameters
Id of the Sailbox, as returned by create. It is sb_ followed by a UUID.
Body
Cron expression with five fields: minute, hour, day of month, month, and day of week. 0 9 * * 1-5 is 9:00 on weekdays.
Shell command each run starts. At most 16 KiB.
IANA time zone the schedule is read in, such as America/New_York. Defaults to UTC.
Working directory each run starts in.
User each run starts as: a user name or numeric uid, optionally with a group after a colon. Defaults to the image's USER, or root when the image sets none. A request made from inside the Sailbox defaults to the calling user, and only root there can set another.
Environment variables each run adds. Names must match [A-Za-z_][A-Za-z0-9_]*, and a value cannot contain a NUL character. Sail stores the values encrypted and never returns them.
Show child attributes
Show child attributes
Seconds after which a run is killed. Without it a run has no time limit.
x >= 1Response
The cron job was added.
Id of the cron job.
Id of the Sailbox.
The five-field cron expression.
IANA time zone the schedule is read in.
Shell command each run starts.
When the next run is due.
When the job was added.
Working directory each run starts in. Absent when none was set.
User each run starts as. Absent when none was set.
Names of the environment variables each run adds, sorted. The values are not returned. Absent when none were set.
Seconds after which a run is killed. Absent when none was set.
When the last run started. Absent until the first run.