From nyxid
Creates and manages cron schedules that fire an Aevatar service on a recurring basis via REST API with NyxID auth. Covers preview, enable/disable, run-now, update, and delete.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nyxid:aevatar-schedulerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You create a **schedule** that fires a published service on a cron expression,
You create a schedule that fires a published service on a cron expression,
authenticated as you (the scope owner) through NyxID. Publish the service first
(aevatar-service-publisher) — you need its identity, an endpoint, and the payload type.
# Drive aevatar THROUGH the NyxID broker: it injects your scope_id claim AND auto-refreshes your
# token. A raw curl to the aevatar backend with ~/.nyxid/access_token resolves NO scope
# (scopeResolved:false) and the stored token expires — it is not a usable path.
# Prerequisite once: the `aevatar` service must be connected — `nyxid service add aevatar`.
# NOTE: the aevatar backend requires `Content-Type: application/json` on writes (POST/PUT) —
# omit it and every write returns HTTP 415 Unsupported Media Type. The helper sets it on
# every call (harmless on bodyless GETs), so the POST/PUT examples below work as written.
aev() { nyxid proxy request aevatar "$@" -H 'Content-Type: application/json'; } # aev "<path>" [-m POST|PUT|DELETE] [-d '<json>'] [--stream]
scopeId=$(aev "api/studio/context" | jq -r .scopeId)
jqis only for convenience — any JSON reader works (replace| jq -r .scopeIdwith| python3 -c 'import sys,json;print(json.load(sys.stdin)["scopeId"])'). All calls go through the NyxID broker (nyxid proxy request aevatar), which injects your scope_id claim and auto-refreshes the token. Reminder: thescopeOwnerNyxIdprecondition below cannot be satisfied by a bare NyxID CLI token — it needs the owner's interactive console NyxID login (broker binding), or creation 400s.
GET /api/scopes/{scopeId}/services returns everything you need per service — copy it off
the entry for your service:
aev "api/scopes/$scopeId/services" \
| jq '.[] | {tenantId, appId, namespace, serviceId, defaultServingRevisionId, invokeReady,
endpoints: [.endpoints[] | {endpointId, requestTypeUrl}]}'
{tenantId, appId, namespace, serviceId}. For a workflow
member the serviceId is member-<memberId>.endpoints[] (payloadTypeUrl = the
endpoint's requestTypeUrl). A workflow member's default endpoint is chat with
type.googleapis.com/aevatar.ai.ChatRequestEvent.defaultServingRevisionId. Required whenever you
send payloadJson (see below).payloadBase64 for a packed
proto). For a chat endpoint, {"prompt":"…"} is accepted.invokeReady is true before scheduling — a schedule against a not-yet-serving
service will fire into nothing.aev "api/schedules/preview" -m POST \
-d '{"cronExpression":"0 9 * * 1-5","timezone":"Asia/Shanghai","count":"5"}' | jq .
Returns the next N fire times so you can confirm the expression means what the user wants.
Use a real IANA timezone; the engine has no implicit local time.
A scheduled service fire happens later, after your current token has expired, so the
platform must be able to re-mint the scope owner's NyxID credential at fire time. That
requires an authenticated NyxID owner binding (urn:nyxid:scope:broker_binding),
established by signing in through the Aevatar console / studio NyxID login (a browser PKCE
authorization_code flow → POST /api/auth/nyxid/finalize). A plain NyxID-CLI token is
not sufficient. Create-time validation does a real token mint, so a missing/revoked
binding fails fast at create with one of:
HTTP 400 — "Authenticated NyxID owner binding is required for scope owner schedule auth…" HTTP 400 — "NyxID binding was revoked for the scheduled subject. (Parameter 'configuration')"
Diagnose before re-logging in — the binding lives on the NyxID side, so check it directly:
NYX=$(tr -d '\n' < ~/.nyxid/base_url); TOK=$(tr -d '\n' < ~/.nyxid/access_token)
curl -s -H "Authorization: Bearer $TOK" "$NYX/api/v1/users/me/broker-bindings" \
| jq -r '.bindings[] | "\(.client_name) scopes=\(.scopes|join(",")) last_used=\(.last_used_at)"'
A non-revoked aevatar binding with the proxy scope means NyxID is healthy and the fault
is Aevatar-side (it can be pinned to a stale binding). A clean console re-login (fully
logged out first) refreshes a revoked binding — finalize replaces it on the revoked/stale
probe path — so that usually clears it; an SSO-cached login may not re-run finalize.
There is no CLI / headless path to establish this binding (NyxID mints broker bindings
only via the authorization_code grant; the only Aevatar writer is the browser finalize).
Tracked at aevatarAI/aevatar#2491 — do not promise a CLI-only way to create a
scopeOwnerNyxId schedule until it lands.
For a recurring run without the browser console, don't use scopeOwnerNyxId scheduling
at all. The published service is already invocable — drive it from an external timer
(cron, launchd, a node) that hits the invoke endpoint with a non-expiring NyxID API key
(nyxid api-key create --scopes proxy; export as NYXID_ACCESS_TOKEN). No broker binding,
no console:
NYXID_ACCESS_TOKEN="$KEY" nyxid proxy request aevatar \
"api/scopes/$scopeId/members/$memberId/invoke/chat:stream" -m POST --stream \
-H 'Content-Type: application/json' -d '{"prompt":"poll"}'
The member invoke endpoint carries scopeId in its path, so it runs even though a bare API
key reports scopeResolved:false on the generic api/studio/context call. Trade-off: the
timer runs on whatever machine you put it on (a cloud cron would live in Aevatar; this does not).
aev "api/schedules" -m POST -d "{
\"displayName\": \"Weekday 9am run\",
\"cronExpression\": \"0 9 * * 1-5\",
\"timezone\": \"Asia/Shanghai\",
\"enabled\": true,
\"serviceInvocation\": {
\"identity\": { \"tenantId\": \"$scopeId\", \"appId\": \"default\", \"namespace\": \"default\", \"serviceId\": \"member-<memberId>\" },
\"endpointId\": \"chat\",
\"payloadTypeUrl\": \"type.googleapis.com/aevatar.ai.ChatRequestEvent\",
\"payloadJson\": $(jq -nc '{prompt:"do the thing"} | tojson'),
\"revisionId\": \"<defaultServingRevisionId>\",
\"auth\": { \"scopeOwnerNyxId\": { \"scope\": \"proxy\" } }
}
}"
ScheduledDispatchConfigurationHttpRequest: cronExpression (required); displayName?,
timezone?, enabled (default true), headers? (string map), and exactly one target:
serviceInvocation (above) or envelope (a raw actor EventEnvelope — advanced).
payloadJsonrequiresrevisionId. If you supplypayloadJsonwithout arevisionId(and the service has no active serving revision), creation fails with 400 "payloadJson requires a revisionId; provide one explicitly or activate a serving revision." Pass the service'sdefaultServingRevisionId.
Workflow-member services: use
payloadBase64, notpayloadJson. Amember-<id>service produced by a Studio bind (the common workflow path) carries a serving revision with no protocol descriptor, sopayloadJsonfails creation with 400 "payloadTypeUrl '…ChatRequestEvent' could not be resolved: revision '…' has no protocol descriptor set." The fix is to send the request as a packed proto inpayloadBase64instead — it bypasses the descriptor-based JSON encoding. The streaming invoke (…/invoke/chat:stream) accepts the{"prompt":"…"}shorthand via a shim, but the scheduler's typed path does not. For aChatRequestEventwithpromptat field 1:# python3 -c "import base64;print(base64.b64encode(bytes([0x0a,len(p:=b'do the thing')])+p).decode())" # → swap the `payloadJson` line for: "payloadBase64": "CgxkbyB0aGUgdGhpbmc=",If your workflow ignores the prompt (e.g. a self-contained poll), any valid
ChatRequestEventpayload triggers the run.
serviceInvocation.auth)scopeOwnerNyxId: { scope } — fire as the scope owner, re-minting their NyxID at
fire time. The right choice for owner-run schedules, but it requires the owner's broker
binding (see Precondition above), otherwise creation 400s.senderNyxId: { subject: { platform, externalUserId, tenant? }, scope } — fire as a
specific external subject. Only when the schedule must run as someone other than the
owner, and that subject already has a durable NyxID binding — otherwise the fire fails at
credential-mint time.sid=$(...) # scheduleId from the create response
aev "api/schedules" | jq '.[] | {scheduleId, displayName, cronExpression, enabled, nextFireUtc}'
aev "api/schedules/$sid" | jq .
aev "api/schedules/$sid:run-now" -m POST # fire once immediately to test
aev "api/schedules/$sid:disable" -m POST # pause
aev "api/schedules/$sid:enable" -m POST # resume
aev "api/schedules/$sid" -m PUT -d '{ ...updated configuration... }'
aev "api/schedules/$sid" -m DELETE # remove
Note the action verbs use a colon (/{scheduleId}:run-now), not a slash.
After :run-now, confirm the fire actually executed — check the service's runs
(GET /api/scopes/{scopeId}/services/{serviceId}/runs) or the observatory
(GET /api/workflow/observatory/runs). A 2xx on the schedule call means accepted, not
succeeded; a fire can still fail later at credential-mint or execution time, so read the
run back before reporting success.
aevatar-service-publisher.aevatar-platform-map.If you cannot complete a step server-side after a real attempt, hand the original request back to your caller rather than fabricating — see the fallback skill in this family.
npx claudepluginhub chronoaiproject/nyxid --plugin nyxidPublishes Aevatar members/teams/workflows as invocable services via NyxID registration, with REST API lifecycle management, verification, and invocation.
Guides SAP BTP Job Scheduling Service development, configuration, and operations including REST API for cron/recurring jobs, OAuth 2.0, multitenancy on Cloud Foundry and Kyma.
Manages scheduled Claude Code tasks: add recurring/one-off skills/prompts/scripts, list/pause/resume/remove, view results/logs, test execution with safety controls and notifications. Cross-platform (macOS/Linux/Windows).