Shell & PowerShell
Drive the Cephable Automate HTTP Server from curl, bash, and PowerShell — one-liners, reusable helper functions, discovery, batch scripts, and scheduled task patterns for macOS, Linux, and Windows.
The fastest way to explore the API, and the easiest way to wire Cephable into cron, Task Scheduler, Shortcuts, Automator, Stream Deck, or a Makefile.
export CEPHABLE_AUTOMATE_KEY='paste-key-from-the-extension-detail-view'
export CEPHABLE_ENDPOINT='http://127.0.0.1:4317'
$env:CEPHABLE_AUTOMATE_KEY = 'paste-key-from-the-extension-detail-view'
$env:CEPHABLE_ENDPOINT = 'http://127.0.0.1:4317'
Do not put the key in the command itself — process command lines are readable by other local processes, and it lands in your shell history. Use an environment variable or read it from a
0600file.
curl one-liners
Health check — the first thing to run when anything is wrong:
curl -sS "$CEPHABLE_ENDPOINT/health" -H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" | jq
Run a task:
curl -sS --max-time 960 "$CEPHABLE_ENDPOINT/v1/runs" \
-H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "List the three largest files in my Downloads folder with their sizes.",
"include": { "trace": false, "events": false }
}' | jq -r '.answer'
--max-time matters: curl has no default timeout, but your shell script or scheduler might kill it. Keep it above the run's timeoutMs (15 minutes by default).
Just the contract value:
curl -sS "$CEPHABLE_ENDPOINT/v1/runs" \
-H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" -H "Content-Type: application/json" \
-d '{ "prompt": "How many PDFs are in my Documents folder?",
"answerContract": "Reply with exactly: FINAL ANSWER: <integer>",
"include": { "steps": false, "trace": false, "events": false } }' \
| jq -r '.finalAnswer'
What did it actually do?
curl -sS "$CEPHABLE_ENDPOINT/v1/runs" \
-H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" -H "Content-Type: application/json" \
-d '{ "prompt": "Organize the screenshots in my Desktop folder into a Screenshots subfolder." }' \
| jq -r '.steps[] | "\(.index). \(.title) [\(.toolName)] -> \(.status)"'
Models:
curl -sS "$CEPHABLE_ENDPOINT/v1/automate/models" -H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" \
| jq -r '.data[] | select(.downloaded and .supportsTools) | "\(.name) (\(.sizeCode)) selected=\(.selected)"'
Pin a model (once per session, not per run):
curl -sS "$CEPHABLE_ENDPOINT/v1/automate/models/select" \
-H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" -H "Content-Type: application/json" \
-d '{ "family": "Gemma", "sizeCode": "M" }' | jq '.selected.name'
Stop whatever is running — safe to call unconditionally:
curl -sS "$CEPHABLE_ENDPOINT/v1/automate/cancel" \
-H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" -H "Content-Type: application/json" \
-d '{ "force": true }' | jq
A prompt too long for a command line — put it in a file:
jq -n --rawfile prompt ./task.md \
'{ prompt: $prompt, include: { trace: false, events: false } }' \
| curl -sS "$CEPHABLE_ENDPOINT/v1/runs" \
-H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" -H "Content-Type: application/json" \
-d @- | jq -r '.answer'
Bodies are capped at 1 MiB. For large inputs, write the data to the workspace folder and have the run read it from there instead of inlining it.
Reusable bash helpers
#!/usr/bin/env bash
# cephable.sh — source this, or run it directly with a prompt argument.
set -euo pipefail
: "${CEPHABLE_AUTOMATE_KEY:?Set CEPHABLE_AUTOMATE_KEY}"
CEPHABLE_BASE_PORT=4317
CEPHABLE_PORT_ATTEMPTS=12
# Cephable moves to the next free port when its preferred one is taken, so sweep the same
# window the app uses and confirm the service name before trusting a listener.
cephable_discover() {
if [[ -n "${CEPHABLE_ENDPOINT:-}" ]]; then printf '%s' "$CEPHABLE_ENDPOINT"; return 0; fi
local port candidate body
for (( i = 0; i < CEPHABLE_PORT_ATTEMPTS; i++ )); do
port=$(( CEPHABLE_BASE_PORT + i ))
candidate="http://127.0.0.1:${port}"
body=$(curl -sS --max-time 3 "${candidate}/health" \
-H "Authorization: Bearer ${CEPHABLE_AUTOMATE_KEY}" 2>/dev/null) || continue
if [[ "$(printf '%s' "$body" | jq -r '.service // empty')" == "cephable-agent" ]]; then
printf '%s' "$candidate"; return 0
fi
done
echo "No Cephable Automate server answered on 127.0.0.1:${CEPHABLE_BASE_PORT}-$(( CEPHABLE_BASE_PORT + CEPHABLE_PORT_ATTEMPTS - 1 )).
Open Cephable and enable Extensions > Cephable features > Build & Extend > Automate HTTP Server." >&2
return 1
}
cephable_health() {
local endpoint; endpoint=$(cephable_discover)
curl -sS --max-time 10 "${endpoint}/health" -H "Authorization: Bearer ${CEPHABLE_AUTOMATE_KEY}"
}
# The user's own panel runs hold the single inference slot too, so wait rather than 409.
cephable_wait_ready() {
local timeout="${1:-300}" deadline health
deadline=$(( $(date +%s) + timeout ))
while (( $(date +%s) < deadline )); do
health=$(cephable_health)
if [[ "$(printf '%s' "$health" | jq -r '.busy')" == "false" ]] &&
[[ "$(printf '%s' "$health" | jq -r '.workflowStatus')" =~ ^(idle|terminated)$ ]]; then
printf '%s' "$health"; return 0
fi
sleep 1
done
echo "Cephable stayed busy for ${timeout}s" >&2
return 1
}
cephable_cancel() {
local endpoint force; endpoint=$(cephable_discover); force="${1:-false}"
curl -sS --max-time 30 "${endpoint}/v1/automate/cancel" \
-H "Authorization: Bearer ${CEPHABLE_AUTOMATE_KEY}" -H "Content-Type: application/json" \
-d "{\"force\": ${force}}"
}
# cephable_run "<prompt>" [timeoutMs] [restrictToWorkspace]
# Prints the run record. Exit 0 only when status is "completed".
cephable_run() {
local prompt="$1" timeout_ms="${2:-600000}" restrict="${3:-false}"
local endpoint payload response status
endpoint=$(cephable_discover)
payload=$(jq -n \
--arg prompt "$prompt" \
--argjson timeoutMs "$timeout_ms" \
--argjson restrict "$restrict" \
'{ prompt: $prompt, timeoutMs: $timeoutMs, restrictToWorkspace: $restrict,
include: { steps: true, trace: false, events: false } }')
response=$(curl -sS --max-time $(( timeout_ms / 1000 + 30 )) "${endpoint}/v1/runs" \
-H "Authorization: Bearer ${CEPHABLE_AUTOMATE_KEY}" -H "Content-Type: application/json" \
-d "$payload")
# A failed run is HTTP 500 with a COMPLETE record; only a body without schemaVersion
# means no run happened.
if [[ "$(printf '%s' "$response" | jq -r '.schemaVersion // empty')" != "1" ]]; then
printf '%s' "$response" | jq -r '.error.message // "The request failed"' >&2
return 2
fi
printf '%s\n' "$response"
status=$(printf '%s' "$response" | jq -r '.status')
[[ "$status" == "completed" ]]
}
# Direct invocation: ./cephable.sh "do the thing"
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
cephable_wait_ready 300 >/dev/null
cephable_run "$1" | jq -r '.finalAnswer // .answer'
fi
Usage:
source ./cephable.sh
cephable_health | jq '{appVersion, modelName, workflowStatus, busy}'
cephable_wait_ready 120 >/dev/null
cephable_run "Summarize ~/notes/today.md in three bullets." | jq -r '.answer'
PowerShell helpers
# Cephable.psm1
$script:BasePort = 4317
$script:PortAttempts = 12
function Get-CephableKey {
if (-not $env:CEPHABLE_AUTOMATE_KEY) { throw 'Set CEPHABLE_AUTOMATE_KEY' }
return $env:CEPHABLE_AUTOMATE_KEY
}
function Resolve-CephableEndpoint {
<# Cephable moves to the next free port when its preferred one is taken,
so sweep the same window and confirm the service name. #>
if ($env:CEPHABLE_ENDPOINT) { return $env:CEPHABLE_ENDPOINT }
$key = Get-CephableKey
for ($offset = 0; $offset -lt $script:PortAttempts; $offset++) {
$candidate = "http://127.0.0.1:$($script:BasePort + $offset)"
try {
$health = Invoke-RestMethod -Uri "$candidate/health" -TimeoutSec 3 `
-Headers @{ Authorization = "Bearer $key" }
if ($health.service -eq 'cephable-agent') { return $candidate }
} catch {
# A 401 still proves a server is listening here. StatusCode is a WebException int in
# PowerShell 5.1 and an HttpStatusCode enum in 7+, so compare the stringified value.
$code = "$($_.Exception.Response.StatusCode)"
if ($code -eq '401' -or $code -eq 'Unauthorized') {
throw "Cephable is listening on $candidate but rejected the access key"
}
# Nothing listening here; keep sweeping.
}
}
throw "No Cephable Automate server answered on 127.0.0.1:$($script:BasePort)-$($script:BasePort + $script:PortAttempts - 1). Enable Extensions > Cephable features > Build & Extend > Automate HTTP Server."
}
function Get-CephableHealth {
$key = Get-CephableKey
Invoke-RestMethod -Uri "$(Resolve-CephableEndpoint)/health" -TimeoutSec 10 `
-Headers @{ Authorization = "Bearer $key" }
}
function Wait-CephableReady {
param([int] $TimeoutSeconds = 300)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
$health = Get-CephableHealth
if (-not $health.busy -and $health.workflowStatus -in @('idle', 'terminated')) { return $health }
Start-Sleep -Seconds 1
}
throw "Cephable stayed busy for $TimeoutSeconds seconds"
}
function Stop-CephableRun {
param([switch] $Force)
$key = Get-CephableKey
Invoke-RestMethod -Method Post -Uri "$(Resolve-CephableEndpoint)/v1/automate/cancel" -TimeoutSec 30 `
-Headers @{ Authorization = "Bearer $key" } -ContentType 'application/json' `
-Body (@{ force = [bool] $Force } | ConvertTo-Json)
}
function Invoke-CephableRun {
param(
[Parameter(Mandatory)] [string] $Prompt,
[string] $TaskId,
[string] $AnswerContract,
[int] $TimeoutMs = 600000,
[switch] $RestrictToWorkspace,
[switch] $AllowDestructiveTools
)
$key = Get-CephableKey
$body = @{
prompt = $Prompt
timeoutMs = $TimeoutMs
restrictToWorkspace = [bool] $RestrictToWorkspace
allowDestructiveTools = [bool] $AllowDestructiveTools
include = @{ steps = $true; trace = $false; events = $false }
}
if ($TaskId) { $body.taskId = $TaskId }
if ($AnswerContract) { $body.answerContract = $AnswerContract }
try {
Invoke-RestMethod -Method Post -Uri "$(Resolve-CephableEndpoint)/v1/runs" `
-Headers @{ Authorization = "Bearer $key" } -ContentType 'application/json' `
-Body ($body | ConvertTo-Json -Depth 6) -TimeoutSec ([int]($TimeoutMs / 1000) + 30)
} catch {
# A failed run is HTTP 500 with a COMPLETE record, so read the body before deciding.
# PowerShell 7 puts it in ErrorDetails.Message; 5.1 needs the response stream.
$text = $_.ErrorDetails.Message
if (-not $text -and $null -ne $_.Exception.Response) {
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$text = $reader.ReadToEnd()
}
$parsed = $null
if ($text) { try { $parsed = $text | ConvertFrom-Json } catch { } }
if ($null -ne $parsed -and $parsed.schemaVersion -eq 1) { return $parsed }
if ($null -ne $parsed -and $parsed.error) { throw $parsed.error.message }
throw
}
}
Export-ModuleMember -Function Get-CephableHealth, Resolve-CephableEndpoint, Wait-CephableReady,
Stop-CephableRun, Invoke-CephableRun
Usage:
Import-Module .\Cephable.psm1
Get-CephableHealth | Select-Object appVersion, modelName, workflowStatus, busy
Wait-CephableReady | Out-Null
$result = Invoke-CephableRun -Prompt 'Count the PDFs in my Documents folder.' `
-AnswerContract 'Reply with exactly: FINAL ANSWER: <integer>'
if ($result.status -eq 'completed') {
$result.finalAnswer
} else {
Write-Error "Run $($result.status): $($result.errorCode)"
$result.steps | Where-Object status -eq 'failed' |
ForEach-Object { Write-Error " $($_.toolName): $($_.resultSummary)" }
}
Scheduled and unattended runs
The same rules apply whether it is cron, Task Scheduler, or a CI job:
- Confine to the workspace.
restrictToWorkspace: trueon anything unattended. - Keep destructive tools off unless you have a sandboxed profile and trusted inputs.
- Wait for readiness, do not retry into a
409. - Cancel on failure, so an abandoned run does not block the user's next one.
- Read the key from a protected file, not the crontab line.
cron (macOS / Linux)
#!/usr/bin/env bash
# nightly-report.sh
set -euo pipefail
export CEPHABLE_AUTOMATE_KEY="$(< "$HOME/.config/cephable/automate.key")" # chmod 600
source "$(dirname "$0")/cephable.sh"
trap 'cephable_cancel true >/dev/null 2>&1 || true' ERR
cephable_wait_ready 600 >/dev/null
cephable_run "Read every .csv in this folder, total the Amount column per file, and write results.md." \
900000 true \
| tee "$HOME/reports/$(date +%F).json" \
| jq -r '"\(.status) in \(.durationMs)ms"'
30 2 * * * /usr/local/bin/nightly-report.sh >> ~/logs/cephable.log 2>&1
Cephable must be running and signed in for the job to succeed — the server is part of the app, not a background service. On a machine that logs out, schedule it in the user session rather than as a system task.
Windows Task Scheduler
# nightly-report.ps1
$ErrorActionPreference = 'Stop'
$env:CEPHABLE_AUTOMATE_KEY = Get-Content "$env:LOCALAPPDATA\cephable\automate.key" -Raw
Import-Module "$PSScriptRoot\Cephable.psm1"
try {
Wait-CephableReady -TimeoutSeconds 600 | Out-Null
$result = Invoke-CephableRun -Prompt 'Read every .csv in this folder and write results.md.' `
-TaskId "nightly-$(Get-Date -Format yyyy-MM-dd)" `
-RestrictToWorkspace -TimeoutMs 900000
$result | ConvertTo-Json -Depth 8 |
Set-Content "$env:USERPROFILE\reports\$(Get-Date -Format yyyy-MM-dd).json" -Encoding utf8
if ($result.status -ne 'completed') { throw "Run $($result.status): $($result.errorCode)" }
} catch {
Stop-CephableRun -Force | Out-Null
throw
}
Register it to run only when the user is logged on — the app has to be running:
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\scripts\nightly-report.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 2:30am
Register-ScheduledTask -TaskName 'Cephable nightly report' -Action $action -Trigger $trigger
macOS Shortcuts and Automator
A Run Shell Script action turns any Shortcut into an Automate task. Read the key from the keychain rather than embedding it:
#!/bin/bash
# Store once: security add-generic-password -a "$USER" -s cephable-automate -w 'the-key'
KEY=$(security find-generic-password -a "$USER" -s cephable-automate -w)
curl -sS --max-time 960 "http://127.0.0.1:4317/v1/runs" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "$(jq -n --arg p "$1" '{prompt: $p, include: {steps: false, trace: false, events: false}}')" \
| jq -r '.answer'
Pass the Shortcut's input as $1, and put the result on the clipboard or into a notification. Because the assistant can type, click, and read the screen, the pairing with an accessible input — a switch, a sensor, a single Shortcut button — is the point of the whole feature.