SDK
Ruby SDK
A dependency-free client for the Atlas REST API, generated from the OpenAPI spec. Works in any Ruby 3.0+ project.
Overview
The Atlas Ruby SDK wraps the same REST surface documented in the API guide. Atlas::Client exposes a method per operation, handles bearer-token auth, and raises a typed error hierarchy on non-2xx responses. It is generated from docs/atlas-openapi.json, so method names track the API exactly.
Method naming
operationId. Listing tasks (operation atlas_get_tasks) is client.atlas_get_tasks(...). The methods overview maps the common resources.Install
Published on RubyGems. Add it with Bundler or install the gem directly.
gem install atlas-sdkbundle add atlas-sdkAuthentication
Every request carries a Personal Access Token as a bearer credential. Tokens start with atlas_pat_ and are scope-bounded and revocable.
require 'atlas'
# Read the token from the environment - never hard-code it in source.
client = Atlas::Client.new(token: ENV.fetch('ATLAS_TOKEN'))Scopes are enforced server-side
403 (insufficient_scope) and raises Atlas::ForbiddenError - the SDK never widens a token. Mint the narrowest scope set that works and widen only when a call is refused.Quickstart
One authenticated request, end to end. Set ATLAS_TOKEN in your environment first.
require 'atlas'
client = Atlas::Client.new(token: ENV.fetch('ATLAS_TOKEN'))
# List endpoints answer an { "items" => [...], "nextCursor" => ... } envelope.
page = client.atlas_get_tasks(query: { limit: 5 })
page['items'].each do |task|
puts "#{task['id']} #{task['title']}"
endCore examples
The everyday task and project operations. Reads answer an { items, nextCursor } envelope; writes take a body hash.
List tasks with filters
# Filters map straight to the REST query string; the server ignores unknown keys.
page = client.atlas_get_tasks(query: {
projectId: 'prj_123',
status: 'IN_PROGRESS',
assigneeId: 'usr_42',
limit: 50,
})
tasks = page['items']Get a task
# Single-resource routes carry the id in the path. Client#request is public, so
# call it directly to fill the id in.
task = client.request('GET', "/tasks/#{task_id}")
puts task['title'], task['status']Single-resource routes
atlas_get_tasks_record method targets /tasks/{id} but does not interpolate the id, so fill it through the public request method as shown. The same applies to update and delete.Create a task
created = client.atlas_post_tasks(body: {
projectId: 'prj_123',
title: 'Review Q3 forecast',
priority: 'HIGH',
dueOn: '2026-05-01T17:00:00Z',
})
puts "created #{created['id']}"Update a task
updated = client.request('PATCH', "/tasks/#{task_id}", body: { status: 'DONE' })Cursor-based pagination
def each_task(client, **filters)
cursor = nil
loop do
query = filters.merge(limit: 100)
query[:cursor] = cursor if cursor
page = client.atlas_get_tasks(query: query)
page['items'].each { |task| yield task }
cursor = page['nextCursor']
break unless cursor
end
end
each_task(client, projectId: 'prj_123') { |task| puts task['id'] }List projects
projects = client.atlas_get_projects(query: { limit: 50 })
projects['items'].each do |project|
puts "#{project['id']} #{project['name']}"
endError handling
Every non-2xx response raises a subclass of Atlas::ApiError. The error carries the HTTP status and the parsed problem+json body.
begin
client.atlas_post_tasks(body: { title: '' })
rescue Atlas::ForbiddenError => err
# 403: the token is missing a scope. The scopes are in the problem body.
puts "need scopes: #{(err.body || {})['requiredScopes']}"
rescue Atlas::UnauthorizedError
# 401: token invalid or revoked - re-authenticate.
raise
rescue Atlas::ApiError => err
# 409 / 422 / 429 and anything else land here with #status set.
problem = err.body || {}
warn "#{err.status} #{problem['title']} #{problem['requestId']}"
(problem['errors'] || []).each { |f| warn "#{f['path']}: #{f['message']}" } if err.status == 422
raise
end| HTTP status | Error class | Meaning |
|---|---|---|
| 400 | Atlas::BadRequestError | Malformed request or query. |
| 401 | Atlas::UnauthorizedError | Token missing, invalid, expired, or revoked. |
| 403 | Atlas::ForbiddenError | Token is missing a required scope. |
| 404 | Atlas::NotFoundError | Resource does not exist or is out of tenant. |
| 409, 422, 429 | ApiError (base) | No dedicated class - branch on the status code and read the body. |
| 5xx | Atlas::ServerError | Transient server fault. Safe to retry. |
Only the statuses above get a dedicated class. Conflicts (409), validation failures (422), and rate limits (429) raise the base Atlas::ApiError, so branch on err.status and read the body. Every error wraps the API's RFC 9457 problem+json envelope:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
x-request-id: f47ac10b-58cc-4372-a567-0e02b2c3d479
{
"type": "https://atlas.dev/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This Personal Access Token is missing one of the required scopes: tasks:write.",
"requiredScopes": ["tasks:write"],
"grantedScopes": ["tasks:read"],
"requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}err.body is that parsed JSON: type, title, status, detail, and a requestId to quote in support tickets. A 422 adds an errors array; a 429 adds a rateLimit object.
Retries and rate limits
Retry 429 and 5xx; never retry 4xx you caused. The 429 body carries the retry delay, so honour it before falling back to backoff.
RETRYABLE = [429, 500, 502, 503, 504].freeze
def with_retries(max_attempts: 5)
attempt = 0
begin
attempt += 1
yield
rescue Atlas::ApiError => err
raise if !RETRYABLE.include?(err.status) || attempt >= max_attempts
rate_limit = (err.body || {})['rateLimit'] || {}
# The 429 body carries the authoritative retry delay, in seconds.
delay = rate_limit['retryAfterSec'] || [2**(attempt - 1), 30].min + rand
sleep(delay)
retry
end
end
page = with_retries { client.atlas_get_tasks(query: { limit: 50 }) }Back off with jitter
retryAfterSec is present, use exponential backoff with a random jitter so a fleet of clients does not retry in lockstep. Cap total attempts - a persistent 429 means you need a higher tier, not more retries.Idempotency
Every write accepts an Idempotency-Key header. Replaying the same key within 24h returns the original response verbatim - safe retries on flaky networks.
require 'atlas'
require 'json'
require 'net/http'
require 'securerandom'
require 'uri'
module Atlas
# The generated client sends a fixed header set; this thin extension adds an
# Idempotency-Key to writes so a retried request replays (24h) instead of
# creating a duplicate.
class IdempotentClient < Client
def write(method, path, body, key:)
uri = URI.parse("#{@base_url}#{path}")
req = Net::HTTP.const_get(method.capitalize).new(uri)
req['Authorization'] = "Bearer #{@token}"
req['Content-Type'] = 'application/json'
req['Idempotency-Key'] = key
req.body = JSON.generate(body)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
JSON.parse(http.request(req).body)
end
end
end
client = Atlas::IdempotentClient.new(token: ENV.fetch('ATLAS_TOKEN'))
# Generate one key per logical write and reuse it on every retry.
key = SecureRandom.uuid
task = client.write('POST', '/tasks', { projectId: 'prj_123', title: 'Ship it' }, key: key)Reuse the key on retry
Configuration
The constructor takes the base URL and separate connect and read timeouts. Override the base URL for staging or a self-hosted deployment.
require 'atlas'
client = Atlas::Client.new(
token: ENV.fetch('ATLAS_TOKEN'),
base_url: 'https://api.atlas.app/v1', # override for staging or self-hosted
open_timeout: 10, # seconds to establish the connection
read_timeout: 30, # seconds to wait for the response
)For a custom User-Agent, request id, or any other header, extend the client as in Idempotency - the transport is a single method you can wrap.
Methods overview
The common task and project operations. The full set - one method per operation - lives in the API reference.
| Resource | REST | SDK method |
|---|---|---|
| List tasks | GET /tasks | atlas_get_tasks(query:) |
| Create task | POST /tasks | atlas_post_tasks(body:) |
| Get task | GET /tasks/{id} | atlas_get_tasks_record * |
| Update task | PATCH /tasks/{id} | atlas_patch_tasks_record * |
| Delete task | DELETE /tasks/{id} | atlas_delete_tasks_record * |
| List projects | GET /projects | atlas_get_projects(query:) |
| Create project | POST /projects | atlas_post_projects(body:) |
* Single-resource methods exist but send the literal {id} path; fill the id through the public request method (see Get a task).
Next steps
The SDK is a thin, faithful layer over the REST API. When you need the exact shape of a request or response, go to the source.
- REST API guideAuth, pagination, idempotency, and rate limits at the protocol level - the contract every SDK speaks./docs/api
- API referenceEvery operation, request body, and response shape - the source of truth for method arguments./docs/api/reference
- MCP for AI agentsHand the same operations to an AI agent over the Model Context Protocol./docs/mcp