HTTP API Reference
Subroutine exposes a RESTful HTTP API that all clients (desktop, Android, Wear OS) use as their sole data layer. There is no proprietary sync protocol — everything goes through these endpoints.
Base URL: https://api.subroutineapp.com
All request and response bodies are JSON. Content-Type: application/json is required
on requests with a body.
Authentication
The API is currently unauthenticated. It is intended for private/self-hosted deployments. Authentication will be added in a future release.
Data model
Action states
An Action.state is a tagged JSON object with a type field:
| State | JSON |
|---|---|
| Backlogged (no date) | {"type": "backlogged", "date": null} |
| Backlogged (date hint) | {"type": "backlogged", "date": "2024-01-15"} |
| Queued, floating | {"type": "queued", "time": "2024-01-15T14:00:00Z", "is_static": false} |
| Queued, static | {"type": "queued", "time": "2024-01-15T14:00:00Z", "is_static": true} |
| Completed | {"type": "completed", "at": "2024-01-15T14:05:00Z"} |
| Skipped | {"type": "skipped"} |
A floating action's time is managed by the pipeline scheduler. A static action is pinned to its time by the user.
Recurrence
{ "unit": "days" | "weeks" | "months" | "years", "every": 1 }Action
{
"id": "018f1e2a-...",
"lineage_id": "018f1e2a-...",
"routine_id": null,
"template_id": null,
"title": "Buy milk",
"content": null,
"duration": null,
"recurrence": null,
"state": { "type": "backlogged", "date": null }
}
lineage_id groups recurring instances of the same action.
duration is nanoseconds (i64) when present.
ActionTemplate
{
"id": "018f1e2a-...",
"lineage_id": "018f1e2a-...",
"title": "Weekly review",
"content": null,
"duration": null,
"recurrence": { "unit": "weeks", "every": 1 }
}Event
{
"id": "018f1e2b-...",
"lineage_id": "018f1e2b-...",
"template_id": null,
"title": "Team standup",
"content": null,
"time": "2024-01-15T14:00:00Z",
"duration": 3600000000000,
"recurrence": null
}Routine
{
"id": "018f1e2c-...",
"title": "Morning routine",
"content": null,
"target": null,
"recurrence": null,
"steps": [
{ "title": "Stretch", "duration": 300000000000 },
{ "title": "Journal", "duration": 600000000000 }
]
}
POST /v1/routines/{id}/instantiate accepts an optional body:
{ "start_time": "2024-01-15T09:00:00Z" }
If omitted, steps start from the current time.
Endpoints
Bulk fetch
| Method | Path | Description | Response |
|---|---|---|---|
GET | /v1/data | Fetch all collections at once | AllData |
AllData shape:
{
"actions": [ Action ],
"events": [ Event ],
"routines": [ Routine ],
"action_templates": [ ActionTemplate ],
"event_templates": [ EventTemplate ]
}
Actions
| Method | Path | Description | Response |
|---|---|---|---|
GET | /v1/actions | List all actions | [Action] |
POST | /v1/actions | Create (server assigns ID, must be backlogged) | 201 Action |
GET | /v1/actions/{id} | Fetch by ID | Action |
PUT | /v1/actions/{id} | Upsert (client assigns ID) | Action |
DELETE | /v1/actions/{id} | Soft-delete | 204 |
POST | /v1/actions/{id}/queue | Backlogged → queued; refreshes pipeline | [Action] changed |
POST | /v1/actions/{id}/backlog | Queued → backlogged | Action |
POST | /v1/actions/{id}/complete | Mark complete; spawns next recurrence if set | CompleteResult |
POST | /v1/actions/{id}/save | Save as a reusable template | Action |
POST | /v1/actions/{id}/clear_duration | Remove the explicit duration | Action |
CompleteResult:
{ "completed": Action, "next": Action | null }
Action templates
| Method | Path | Description | Response |
|---|---|---|---|
GET | /v1/actions/templates | List all templates | [ActionTemplate] |
POST | /v1/actions/templates | Create template | 201 ActionTemplate |
GET | /v1/actions/templates/{id} | Fetch by ID | ActionTemplate |
PUT | /v1/actions/templates/{id} | Upsert template | ActionTemplate |
DELETE | /v1/actions/templates/{id} | Soft-delete | 204 |
Events
| Method | Path | Description | Response |
|---|---|---|---|
GET | /v1/events | List all events | [Event] |
POST | /v1/events | Create event | 201 Event |
GET | /v1/events/{id} | Fetch by ID | Event |
PUT | /v1/events/{id} | Upsert event | Event |
DELETE | /v1/events/{id} | Soft-delete | 204 |
POST | /v1/events/{id}/save | Save as a reusable template | Event |
Event templates
| Method | Path | Description | Response |
|---|---|---|---|
GET | /v1/events/templates | List all templates | [EventTemplate] |
POST | /v1/events/templates | Create template | 201 EventTemplate |
GET | /v1/events/templates/{id} | Fetch by ID | EventTemplate |
PUT | /v1/events/templates/{id} | Upsert template | EventTemplate |
DELETE | /v1/events/templates/{id} | Soft-delete | 204 |
Routines
| Method | Path | Description | Response |
|---|---|---|---|
GET | /v1/routines | List all routines | [Routine] |
POST | /v1/routines | Create routine (server assigns ID) | 201 Routine |
GET | /v1/routines/{id} | Fetch by ID | Routine |
PUT | /v1/routines/{id} | Upsert routine | Routine |
DELETE | /v1/routines/{id} | Soft-delete | 204 |
POST | /v1/routines/{id}/instantiate | Create static queued actions from steps | [Action] |
Pipeline
| Method | Path | Description | Response |
|---|---|---|---|
POST | /v1/pipeline/refresh | Requeue missed/floating actions | [Action] changed |
POST | /v1/pipeline/expedite | Pull floating actions within 6h toward now | [Action] changed |
Live updates (SSE)
GET /v1/changes/stream
A persistent Server-Sent Events
stream. Each event is a JSON object with a type field. The client should treat each
event as a signal to re-fetch the relevant collection — events carry no payload.
data: {"type":"actions_changed"}
data: {"type":"pipeline_changed"}
data: {"type":"events_changed"}
data: {"type":"routines_changed"}
data: {"type":"action_templates_changed"}
data: {"type":"event_templates_changed"}
A keep-alive ping comment is sent every 15 seconds to prevent proxy timeouts.
Browser (JavaScript):
const es = new EventSource("https://api.subroutineapp.com/v1/changes/stream");
es.onmessage = (e) => {
const { type } = JSON.parse(e.data);
if (type === "actions_changed") fetchActions();
};
Nushell:
curl -sN https://api.subroutineapp.com/v1/changes/stream
Code examples
Nushell
let server = "https://api.subroutineapp.com"
# Fetch everything at once
http get $"($server)/v1/data" | select actions events routines
# Create a backlogged action
let id = (random uuid)
http post $"($server)/v1/actions" {
id: $id
lineage_id: $id
routine_id: null
template_id: null
title: "Buy milk"
content: null
duration: null
recurrence: null
state: {type: backlogged, date: null}
}
# Queue it, complete it
http post $"($server)/v1/actions/($id)/queue" {}
http post $"($server)/v1/actions/($id)/complete" {}
# Expedite the pipeline
http post $"($server)/v1/pipeline/expedite" {}curl
SERVER="https://api.subroutineapp.com"
ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
# Create an action
curl -X POST "$SERVER/v1/actions" \
-H "Content-Type: application/json" \
-d "{\"id\":\"$ID\",\"lineage_id\":\"$ID\",\"routine_id\":null,\"template_id\":null,\"title\":\"Daily standup\",\"content\":null,\"duration\":null,\"recurrence\":{\"unit\":\"days\",\"every\":1},\"state\":{\"type\":\"backlogged\",\"date\":null}}"
# List actions
curl "$SERVER/v1/actions" | jq '.[].title'
# SSE stream
curl -sN "$SERVER/v1/changes/stream"Python
import httpx, uuid
server = "https://api.subroutineapp.com"
# Fetch all data
data = httpx.(f"{}/v1/data").()
print([["title"] for in["actions"]])
# Create and complete an action
action_id = str(.())
httpx.(f"{}/v1/actions", json={
"id":, "lineage_id":,
"routine_id": None, "template_id": None,
"title": "Write tests", "content": None,
"duration": None, "recurrence": None,
"state": {"type": "backlogged", "date": None},
})
httpx.(f"{}/v1/actions/{}/queue")
httpx.(f"{}/v1/actions/{}/complete")
# Subscribe to live changes
with httpx.("GET", f"{}/v1/changes/stream") as r:
for line in r.():
if line.("data:"):
print()
Self-hosting
The server is a single statically-linked binary for Linux x86-64, backed by PostgreSQL.
# Required environment variables
DATABASE_URL=postgresql://subroutine:<password>@127.0.0.1/subroutine
BIND_ADDR=0.0.0.0:3000
./simple-server # runs migrations automatically on startup
See the GitHub repository for build and deployment instructions.