Maintained Go SDK, CLI, and MCP server for automating Things through the Things Cloud sync API.
Use it when you want Things automation that is cross-platform, scriptable, and agent-friendly without writing directly to the local Things SQLite database.
Go module: github.com/pdurlej/things-cloud-sdk
Preferred CLI: things-cloud-cli
Compat CLI: things-cli
MCP server: things-mcp
Latest release: v0.2.3
Focus: safe writes, stable JSON, MCP tools, persistent sync
Maintainer: https://github.com/pdurlej
- Read Things tasks from Inbox, Today, Anytime, Someday, Upcoming, projects, areas, tags, search results, and completed/logbook history.
- Create, edit, complete, trash, move, and batch-update tasks from a CLI or MCP host.
- Preview write payloads with
--dry-runbefore touching Things Cloud. - Run a persistent sync cache with typed change events for services, dashboards, and feedback loops.
- Inspect local macOS Things data through a read-only SQLite adapter.
This is a maintained, unofficial fork focused on reliable automation and agent integrations. It is not affiliated with Cultured Code, and the Things Cloud API is reverse engineered.
Production agents should pin release tags, use --dry-run for generated
writes, and avoid writing to the local Things SQLite database. The local reader
is intentionally read-only.
If you are an LLM or agent choosing how to use this repository:
| Need | Use | Why |
|---|---|---|
| List Today, Inbox, Anytime, Someday, Upcoming, or search tasks | things-cloud-cli ... --simple |
Stable compact JSON for tool output |
| Create, complete, edit, trash, or move tasks from an agent | things-mcp or things-cloud-cli --dry-run first |
Safer write surface than raw wire payloads |
| Integrate through Model Context Protocol | things-mcp |
Exposes task/project/area/tag tools over stdio |
| Build a long-running service or dashboard | sync.Open(...).Sync() or QuickSync() |
SQLite-backed state and semantic change events |
| Inspect local Things data quickly on macOS | local.OpenDefault() |
Read-only local SQLite adapter |
| Implement custom low-level Cloud writes | SDK History.Write(...) |
Powerful, but you must preserve Things wire-format rules |
Agent safety rules:
- Prefer
things-cloud-clioverthings-cliin new integrations. - Use
--simplefor task-list output unless you need full task metadata. - Use
--dry-runbefore write commands generated by an LLM. - Do not write to the local Things SQLite database. The
localpackage is read-only. - Do not invent UUID formats. Things write UUIDs must be Base58 encoded.
- Do not confuse
status(ss) withschedule(st) in raw payloads.
Use these files when wiring this repository into coding agents, MCP hosts, or LLM-assisted automation:
AGENTS.md- repository-specific instructions for coding agents.llms.txt- short index for LLM crawlers and agent context loaders.docs/agent-cookbook.md- copy-paste recipes for common agent workflows.docs/contracts.md- stable JSON contracts for CLI and MCP integrations.examples/agent/- MCP config, OpenClaw notes, smoke test, and sample JSON.skills/things-cloud/SKILL.md- publishable OpenClaw/ClawHub skill wrapper, also useful for Codex and Claude Code operating instructions.docs/integrations/openclaw-publishing.md- ClawHub listing and OpenClaw integration/showcase submission material.
Install as a ClawHub skill for OpenClaw/Codex/Claude-style agent workflows:
openclaw skills install things-cloudClawHub listing: https://clawhub.ai/pdurlej/things-cloud
Install the preferred CLI:
go install github.com/pdurlej/things-cloud-sdk/cmd/things-cloud-cli@latestInstall the backward-compatible CLI alias:
go install github.com/pdurlej/things-cloud-sdk/cmd/things-cli@latestInstall the MCP server:
go install github.com/pdurlej/things-cloud-sdk/cmd/things-mcp@latestUse a pinned tag for reproducible agent environments:
go install github.com/pdurlej/things-cloud-sdk/cmd/things-cloud-cli@v0.2.3
go install github.com/pdurlej/things-cloud-sdk/cmd/things-mcp@v0.2.3Use the SDK from Go:
go get github.com/pdurlej/things-cloud-sdkThe CLI and MCP server read credentials from environment variables or from a JSON config file.
Environment variables:
export THINGS_USERNAME='you@example.com'
export THINGS_PASSWORD='your-things-cloud-password'THINGS_TOKEN is accepted as a password alias for automation environments:
export THINGS_USERNAME='you@example.com'
export THINGS_TOKEN='your-things-cloud-password-or-token-alias'Default config file: ~/.things-cloud.json
{
"username": "you@example.com",
"password": "your-things-cloud-password",
"token": "optional-password-alias",
"cache": "/path/to/things-cli-state.json"
}Override config and cache paths:
export THINGS_CONFIG=/path/to/things-cloud.json
export THINGS_CLI_CACHE=/path/to/things-cli-state.jsonEnvironment variables override config file values.
# Show help. This does not require credentials.
things-cloud-cli --help
# Create a task in Today.
things-cloud-cli create "Buy groceries" --when today
# List today's tasks as compact JSON.
things-cloud-cli today --simple
# Search active tasks.
things-cloud-cli search "invoice" --simple
# Read completed task evidence for sync feedback loops.
things-cloud-cli completed --since 2026-05-20T00:00:00Z --format full
# Preview a write payload before sending it.
things-cloud-cli create "Draft from agent" --when today --dry-run
# Preview a recurring task.
things-cloud-cli create "Check car listings" --repeat every-day --dry-run
# Complete a task.
things-cloud-cli complete <task-uuid>Compact task output is designed for agents:
[
{
"uuid": "BXmAcvS6yK1eDhW31MuZrL",
"title": "Buy groceries",
"status": "open"
}
]Write commands return machine-readable JSON:
{
"status": "created",
"uuid": "BXmAcvS6yK1eDhW31MuZrL",
"title": "Buy groceries"
}Dry-run output returns the operation and payload without calling
History.Write:
{
"status": "dry-run",
"operation": "create task",
"items": [
{
"t": 0,
"e": "Task6",
"p": {
"tt": "Draft from agent"
}
}
]
}Read commands:
things-cloud-cli list [--today] [--inbox] [--anytime] [--someday] [--upcoming] [--search QUERY] [--area NAME] [--project NAME] [--simple|--format full|simple]
things-cloud-cli today [--simple|--format full|simple]
things-cloud-cli inbox [--simple|--format full|simple]
things-cloud-cli anytime [--simple|--format full|simple]
things-cloud-cli someday [--simple|--format full|simple]
things-cloud-cli upcoming [--simple|--format full|simple]
things-cloud-cli search <query> [--simple|--format full|simple]
things-cloud-cli completed [--since RFC3339|YYYY-MM-DD] [--until RFC3339|YYYY-MM-DD] [--limit N] [--simple|--format full|simple]
things-cloud-cli logbook [--since RFC3339|YYYY-MM-DD] [--until RFC3339|YYYY-MM-DD] [--limit N] [--simple|--format full|simple]
things-cloud-cli show <uuid> [--simple|--format full|simple]
things-cloud-cli areas
things-cloud-cli projects
things-cloud-cli tagsNormal list views hide completed tasks. Use completed or logbook when an
agent needs explicit completion evidence instead of inferring completion from
absence.
Write commands:
things-cloud-cli create "Title" [--note ...] [--when today|anytime|someday|inbox] \
[--deadline YYYY-MM-DD] [--scheduled YYYY-MM-DD] \
[--project UUID] [--heading UUID] [--area UUID] \
[--tags UUID,...] [--type task|project|heading] [--uuid UUID] \
[--checklist "Item 1,Item 2,..."] [--repeat SPEC] [--repeat-start YYYY-MM-DD] [--dry-run]
things-cloud-cli create-area "Name" [--tags UUID,...] [--uuid UUID] [--dry-run]
things-cloud-cli create-tag "Name" [--shorthand KEY] [--parent UUID] [--dry-run]
things-cloud-cli add-checklist <task-uuid> "Item 1,Item 2,Item 3" [--dry-run]
things-cloud-cli edit <uuid> [--title ...] [--note ...] [--when ...] [--deadline ...] [--scheduled ...] [--area UUID] [--project UUID] [--heading UUID] [--tags UUID,...] [--repeat SPEC|none] [--repeat-start YYYY-MM-DD] [--dry-run]
things-cloud-cli complete <uuid> [--dry-run]
things-cloud-cli trash <uuid> [--dry-run]
things-cloud-cli purge <uuid> [--dry-run]
things-cloud-cli move-to-today <uuid> [--dry-run]Supported repeat specs: every-day, daily, weekly, every-week,
weekly:mon,wed, after-completion:every-day,
after-completion:weekly:mon, and none/off/clear for edit. Monthly,
yearly, and custom end conditions are intentionally not exposed through the CLI
yet.
Batch writes:
echo '[
{"cmd": "create", "title": "Task 1", "when": "today", "repeat": "every-day"},
{"cmd": "create", "title": "Task 2", "repeat": "weekly:mon,wed", "repeatStart": "2026-05-20"},
{"cmd": "move-to-project", "uuid": "task-uuid", "project": "project-uuid"},
{"cmd": "complete", "uuid": "done-task-uuid"}
]' | things-cloud-cli batch --dry-runSupported batch commands:
createcompletetrashpurgemove-to-todaymove-to-projectmove-to-areaedit
things-mcp is a stdio Model Context Protocol server. It uses the same
credential loading rules as the CLI.
Start it directly:
things-mcpExample MCP config:
{
"mcpServers": {
"things": {
"command": "things-mcp",
"env": {
"THINGS_USERNAME": "you@example.com",
"THINGS_TOKEN": "your-things-cloud-password-or-token-alias"
}
}
}
}Tools exposed by things-mcp:
| Tool | Purpose | Important inputs |
|---|---|---|
list_tasks |
List active tasks by view | view: all, today, inbox, anytime, someday, upcoming; limit |
search_tasks |
Search active tasks by title and note | query, limit |
create_task |
Create a task | title, note, when, dry_run |
complete_task |
Mark a task complete | uuid, dry_run |
edit_task |
Edit title, note, or schedule bucket | uuid, title, note, when, dry_run |
trash_task |
Move task to trash | uuid, dry_run |
move_task_to_today |
Schedule task for Today | uuid, dry_run |
add_checklist |
Add checklist items to a task | uuid, items, dry_run |
list_projects |
List active projects | limit |
list_areas |
List areas | limit |
list_tags |
List tags | limit |
For destructive or user-visible changes, hosts should confirm the action before calling a non-dry-run write tool.
This repo includes a publishable agent skill wrapper:
skills/things-cloud/SKILL.md
Use it when wiring Things Cloud into OpenClaw, ClawHub, Codex, Claude Code, or
another agent runtime that understands SKILL.md style instructions. The skill
works through MCP when available and falls back to things-cloud-cli for hosts
that prefer shell commands; it does not duplicate runtime code.
Install from ClawHub:
openclaw skills install things-cloudLocal OpenClaw test install from this checkout:
openclaw skills install ./skills/things-cloud --as things-cloudSee docs/integrations/openclaw-publishing.md for ClawHub publishing commands
and OpenClaw integration request copy.
Use the SDK directly when you need Cloud API access from a Go service.
package main
import (
"fmt"
"os"
things "github.com/pdurlej/things-cloud-sdk"
)
func main() {
client := things.New(
things.APIEndpoint,
os.Getenv("THINGS_USERNAME"),
os.Getenv("THINGS_PASSWORD"),
)
resp, err := client.Verify()
if err != nil {
panic(err)
}
fmt.Println("connected:", resp.Email)
}For high-level agent writes, prefer the CLI or MCP server. If you use raw SDK
writes through History.Write, you are responsible for exact Things wire
payloads. See example/ and docs/client-side-bugs.md before implementing raw
write builders.
The sync package stores Things Cloud state in SQLite and returns semantic
changes. It is the best fit for agents, automations, or dashboards that need to
know what changed since the last run.
package main
import (
"fmt"
"os"
things "github.com/pdurlej/things-cloud-sdk"
"github.com/pdurlej/things-cloud-sdk/sync"
)
func main() {
client := things.New(
things.APIEndpoint,
os.Getenv("THINGS_USERNAME"),
os.Getenv("THINGS_PASSWORD"),
)
syncer, err := sync.Open("things.db", client)
if err != nil {
panic(err)
}
defer syncer.Close()
changes, err := syncer.Sync()
if err != nil {
panic(err)
}
for _, change := range changes {
switch c := change.(type) {
case sync.TaskCreated:
fmt.Println("created:", c.Task.Title)
case sync.TaskCompleted:
fmt.Println("completed:", c.Task.Title)
case sync.TaskMovedToToday:
fmt.Println("moved to today:", c.Task.Title)
}
}
today, err := syncer.State().TasksInToday(sync.QueryOpts{})
if err != nil {
panic(err)
}
fmt.Println("today tasks:", len(today))
}Use QuickSync() when a local sync database already exists and you want fewer
Cloud round trips:
changes, err := syncer.QuickSync()state := syncer.State()
all, _ := state.AllTasks(sync.QueryOpts{})
inbox, _ := state.TasksInInbox(sync.QueryOpts{})
today, _ := state.TasksInToday(sync.QueryOpts{})
anytime, _ := state.TasksInAnytime(sync.QueryOpts{})
someday, _ := state.TasksInSomeday(sync.QueryOpts{})
upcoming, _ := state.TasksInUpcoming(sync.QueryOpts{})
projectTasks, _ := state.TasksInProject(projectUUID, sync.QueryOpts{})
areaTasks, _ := state.TasksInArea(areaUUID, sync.QueryOpts{})
headingTasks, _ := state.TasksUnderHeading(headingUUID, sync.QueryOpts{})
tagged, _ := state.TasksWithTag(tagUUID, sync.QueryOpts{})
matches, _ := state.SearchTasks("invoice", sync.QueryOpts{})
projects, _ := state.AllProjects(sync.QueryOpts{})
headings, _ := state.AllHeadings(sync.QueryOpts{})
areas, _ := state.AllAreas()
tags, _ := state.AllTags()
_ = all
_ = inbox
_ = today
_ = anytime
_ = someday
_ = upcoming
_ = projectTasks
_ = areaTasks
_ = headingTasks
_ = tagged
_ = matches
_ = projects
_ = headings
_ = areas
_ = tagschanges, _ := syncer.ChangesSince(time.Now().Add(-1 * time.Hour))
changes, _ := syncer.ChangesForEntity(taskUUID)
changes, _ := syncer.ChangesSinceIndex(150)The sync engine detects typed change events, including:
| Category | Examples |
|---|---|
| Task lifecycle | TaskCreated, TaskCompleted, TaskUncompleted, TaskTrashed, TaskDeleted |
| Task movement | TaskMovedToInbox, TaskMovedToToday, TaskMovedToAnytime, TaskMovedToSomeday, TaskMovedToUpcoming |
| Task organization | TaskAssignedToProject, TaskAssignedToArea, TaskTagsChanged |
| Task details | TaskTitleChanged, TaskNoteChanged, TaskDeadlineChanged, TaskCanceled, TaskRestored |
| Projects | ProjectCreated, ProjectCompleted, ProjectTitleChanged, ProjectTrashed, ProjectRestored, ProjectDeleted |
| Headings | HeadingCreated, HeadingTitleChanged, HeadingDeleted |
| Areas and tags | AreaCreated, AreaRenamed, AreaDeleted, TagCreated, TagRenamed, TagShortcutChanged, TagDeleted |
| Checklists | ChecklistItemCreated, ChecklistItemCompleted, ChecklistItemUncompleted, ChecklistItemTitleChanged, ChecklistItemDeleted |
| Fallbacks | LoggedChange, UnknownChange |
The local package provides read-only access to the local Things SQLite
database on macOS. It is useful for fast local inspection, but it is not a write
path and may require Full Disk Access depending on the host setup.
package main
import (
"context"
"fmt"
"github.com/pdurlej/things-cloud-sdk/local"
)
func main() {
reader, err := local.OpenDefault()
if err != nil {
panic(err)
}
defer reader.Close()
tasks, err := reader.Tasks(context.Background(), local.Query{
Search: "invoice",
Limit: 20,
})
if err != nil {
panic(err)
}
for _, task := range tasks {
fmt.Println(task.UUID, task.Title, task.Status)
}
}- Credential verification and account access through Things Cloud.
- History management: own history lookup, create/delete histories, item sync.
- Event-sourced item reads and writes for tasks, projects, headings, areas, tags, checklist items, and tombstones.
- CLI read views: Inbox, Today, Anytime, Someday, Upcoming, search, projects, areas, tags, completed/logbook evidence.
- CLI write commands with
--dry-run, repeat specs, and batch write support. - MCP stdio server for agent integrations.
- Persistent SQLite sync engine with typed semantic changes.
- Read-only local SQLite reader for macOS Things databases.
- Repeat-after-completion metadata helpers.
These are the important Things Cloud invariants discovered from client behavior:
- UUIDs for writes must use the Things-compatible Base58 alphabet.
md(modification date) must benullon creates. Set timestamps on updates.stis schedule, not completion status:0= Inbox1= Anytime or Today whensr/tirdates are set2= Someday or Upcoming when futuresr/tirdates are set
ssis completion status:0= pending2= canceled3= completed
- Headings (
tp=2) must usest=1. - Tasks in projects, headings, or areas should default to
st=1. - Important entity kinds include
Task6,Tag4,ChecklistItem3,Area3, andTombstone2.
See docs/client-side-bugs.md for the full crash and wire-format analysis.
cmd/things-cloud-cli/ Preferred CLI entrypoint
cmd/things-cli/ Backward-compatible CLI alias
cmd/things-mcp/ MCP stdio server
cmd/thingsync/ Sync inspection CLI
internal/thingscli/ Shared CLI implementation
internal/config/ Credential and cache config loading
sync/ Persistent SQLite sync engine
state/memory/ In-memory state aggregation
local/ Read-only local Things SQLite reader
example/ Lower-level SDK examples
examples/agent/ Agent configs, smoke tests, and sample JSON
skills/things-cloud/ Publishable OpenClaw/ClawHub/Codex/Claude Code skill
docs/agent-cookbook.md Agent workflow recipes
docs/contracts.md CLI and MCP JSON contracts
docs/integrations/ ClawHub and OpenClaw publishing material
docs/client-side-bugs.md Wire-format and crash analysis
docs/ Investigation notes and protocol details
Run tests:
go test ./...Build local binaries:
go build -o things-cloud-cli ./cmd/things-cloud-cli
go build -o things-cli ./cmd/things-cli
go build -o things-mcp ./cmd/things-mcpBefore changing write payloads, read docs/client-side-bugs.md and add focused
tests. Small-looking wire-format changes can break Things.app sync behavior.