spotify-cli

spotify-cli / Docs

Triggers

A trigger is a checkpoint in playback. When it is reached, spotify-daemon sends a Ting to the Silicon that created it. Use them to act at the right moment: start wrapping up before a song ends, queue the next thing at the halfway mark, or run something when a song is over.

Create one

Exactly one condition:

Flag Fires when Example
--remaining T at most T is left --remaining 30s, --remaining 1:00
--remaining P% at most P% of the track is left --remaining 25%
--elapsed T / --at T at least T has played --elapsed 1:30, --at 2:00
--elapsed P% the P% mark is passed --elapsed 50%
--end the song finishes by playing to its end --end
--change the song stops being current for any reason --change

Times: 90, 90s, 1:30, 1m30s, 250ms, 1:02:03. Percentages: 0%–100%.

Scope (--scope, default current):

Scope Watches Fires
current only the song playing now once, then the trigger completes
every every song (ads excluded) on every song, until removed or --times N
track --track <uri> every play of one track on every play, until removed or --times N

Other options: --times N / --once, --note TEXT (up to 1000 characters, echoed in the notification), --label NAME, --no-expiry-notice, --local.

spotify trigger add --remaining 30s --note 'start wrapping up the meeting'
spotify trigger add --elapsed 50% --scope every --times 3 --label halfway
spotify trigger add --end --scope track --track spotify:track:0BxE4FqsDD1Ot4YuBXwAPp

The response contains the trigger id (trg_…). Inspect with spotify trigger show <id>, list with spotify trigger list (add --all for finished ones), remove with spotify trigger remove <id> or spotify trigger clear.

Exact rules

These are implemented in the pure engine (silicon_spotify_client::trigger) and unit-tested.

What you receive

Type spotify.trigger.fired (or spotify.trigger.expired). Stemcell delivers it to your flow as one item of request.tings:

{
  "id": "msg_…",
  "type": "spotify.trigger.fired",
  "key": "si:you/trg_…/7/fired",
  "data": {
    "outcome": "fired",
    "trigger": {"id": "trg_…", "label": "halfway", "condition": "elapsed", "threshold": "50%",
                "description": "50% mark passed", "scope": "every", "note": "…",
                "fired": 1, "times": 3, "final": false},
    "track": {"uri": "spotify:track:…", "name": "…", "artist": "…", "album": "…",
              "duration_ms": 253586, "url": "https://open.spotify.com/track/…", "artwork_url": "…"},
    "playback": {"position_ms": 126900, "position": "2:06", "remaining_ms": 126686,
                 "remaining": "2:06", "progress": 0.5},
    "reason": null,
    "at": "2026-09-26T10:00:00.000Z"
  },
  "metadata": {"isi": "planner", "app": "spotify", "app_version": "0.1.0", "host": "studio-mac"}
}

metadata.isi is the ISI environment variable of the process that created the trigger (or the notify_isi setting). It is a routing hint for your flow, never authority. Routing example: spotify docs ting.

Delivery, retries and history

Firings go to a durable outbox. The daemon opens your session store under its lock, refreshes the session if needed, and asks the backend to send the Ting with your identity. Transient failures retry with backoff (10 s, doubling, at most 10 min) for one hour with the same Ting key, so a notification is never duplicated; after an hour a playback checkpoint is stale and the firing is marked failed. Permanent refusals (recipient_not_registered, reconsent_required, testing_selection_changed, …) stop retrying at once and show in spotify trigger history with the reason and the fix. Retry one by hand with spotify trigger retry <firing-id>.

Without Ting

--local records firings only in the daemon. Wait for one synchronously:

id=$(spotify trigger add --remaining 10s --local --json | jq -r .trigger.id)
spotify trigger wait "$id" --timeout 10m --json

trigger wait also works for Ting triggers; it returns when the firing is created, or as soon as the trigger finishes without firing (expired silently, removed).

Troubleshooting

Code Meaning Fix
threshold_passed the checkpoint is already behind the current song later checkpoint, --end, --scope every
nothing_playing current scope needs a song start one, or use --scope every
not_authenticated no login in this home spotify login '<SLT>', or --local
recipient_not_registered Ting has no grant for you spotify ting register
reconsent_required your session lacks Ting scopes log in again approving all scopes

Offline: spotify docs triggers · Source: docs/triggers.md