staqlydocs
CLI

Scripting recipes

Use the staqly CLI in shell scripts and cron jobs to queue posts, publish and wait, react to events and keep channels healthy.

Edit on GitHub

These recipes show the staqly command doing real work: a cron job that queues a post, a pipeline that publishes and waits for the result, a loop that reacts to events, and a check that every channel still works. They assume a running server (see the CLI overview): against staqly serve on the same computer the CLI finds the access token by itself; add --profile or STAQLY_API_URL / STAQLY_API_KEY for Docker or a cloud server.

Queue a post from a script

Install the CLI and point it at your server

curl -fsSL https://raw.githubusercontent.com/staqly/staqly/main/scripts/install.sh | sh
staqly login --url https://app.staqly.ai --key "$STAQLY_API_KEY"   # cloud; skip next to staqly serve

Write a draft into a collection

./generate-tip.sh | staqly posts create "Daily tips" --stdin --channel Announcements --tag tips --idempotency-key "tip-$(date +%F)"

The post is a draft; the collection's schedule publishes it at the next slot. The idempotency key makes a re-run of the same day's job harmless.

Or give it a time

staqly posts create "Daily tips" --content @tip.md --channel Announcements --publish-at "+1d"   # or "2026-11-03 09:00", RFC 3339, +2h

Publish and wait for the result

posts publish --wait returns when every channel confirmed or one failed, so a script can branch on the exit code:

set -e
POST=$(staqly posts create Releases --content @notes.md --channel Blog --channel Announcements --json | jq -r .id)
staqly posts preview "$POST"                      # see what each platform objects to before publishing

if staqly posts publish "$POST" --wait --timeout 10m; then
  echo "published"
else
  case $? in
    2) echo "a channel failed"; staqly posts get "$POST" ;;
    3) echo "still publishing after 10 minutes" ;;
  esac
fi

Act as your own publisher

A channel on your own automation hands posts to you. A script can poll what waits, publish it wherever it should go, and report back:

while :; do
  staqly work --json | jq -c '.data[] | select(.kind=="target")' | while read -r w; do
    POST=$(jq -r .post.id <<<"$w"); CH=$(jq -r .channel.id <<<"$w")
    if URL=$(./publish-somewhere.sh "$(jq -r .post.content <<<"$w")"); then
      staqly posts result "$POST" "$CH" --status published --url "$URL"
    else
      staqly posts result "$POST" "$CH" --status failed --error "publisher exited $?"
    fi
  done
  sleep 30
done

staqly work is GET /v1/work: targets on your own automation waiting for a result, and schedules that ask for new posts. A webhook on post.publish_requested replaces the polling when your script can receive HTTP.

Answer a schedule that asks for posts

staqly work --json | jq -c '.data[] | select(.kind=="posts")' | while read -r w; do
  COL=$(jq -r .collection.id <<<"$w")
  ./write-three-drafts.sh | while read -r line; do
    staqly posts create "$COL" --content "$line"
  done
done

The schedule's next run publishes the first new draft. An agent does the same through list_requested_work; see Let an agent fill a collection.

React to events in a shell loop

events tail --follow prints new events as they happen; with --json each one is a JSON line:

staqly events tail --follow --type 'post.*' --json | while read -r ev; do
  case $(jq -r .type <<<"$ev") in
    post.published) jq -r '.data.post.targets[].external_url' <<<"$ev" | xargs -I{} echo "live at {}" ;;
    post.failed)    jq -r '.data.post.id' <<<"$ev" | xargs -I{} ./notify-me.sh "post {} failed" ;;
  esac
done

For anything that must not miss an event, use a webhook instead; the loop is for a terminal or a quick experiment.

Keep channels healthy

# nightly: test every channel, exit non-zero if one needs reconnecting
staqly channels list --json | jq -r '.data[].id' | while read -r ch; do
  staqly channels test "$ch" || echo "reconnect $ch"
done

# retry whatever failed in the queue
staqly jobs list --status failed --json | jq -r '.data[].id' | xargs -n1 staqly jobs retry

An alert on channel.needs_reconnect (one of the defaults) tells you the same without a script.

Next steps

On this page