AI Job Scheduling
The AI endpoints turn a natural-language description ("follow up two days after the demo", "remind the sales team every Monday at 9am") into job configurations. There are two ways to use them:
POST /ai/promptreturns proposed job configurations. You decide what to do with them, typically by callingPOST /jobs.POST /ai/scheduleruns the same pipeline and then creates the project, picks an executor and creates the jobs in one call.
Both run an intent guardrail first: a classifier decides whether the text is actually a scheduling request. You can call that classifier on its own with POST /ai/prompt/classify. All AI endpoints require the execute scope.
Endpoints
| Method | Path | Scope | Notes |
|---|---|---|---|
POST | /ai/prompt | execute | Generate job configurations |
POST | /ai/schedule | execute | Generate and create jobs → 201 |
POST | /ai/prompt/classify | execute | Intent guardrail only |
POST | /ai/suggestions/analyze | execute | See Conversation Suggestions |
POST | /ai/suggestions/time | execute | Recommend send times |
GET | /ai/prompt-requests | read | Audit log of prompt runs |
GET | /ai/models | read | Approved model catalog |
GET / PUT | /ai/settings | read / write | Bring-your-own-key configuration |
Quotas, credits and rate-limit headers
Two independent limits apply:
- Monthly request quotas.
POST /ai/promptandPOST /ai/scheduledraw from the account's prompt quota;POST /ai/prompt/classifyandPOST /ai/suggestions/analyzedraw from the classify quota. Each is 1,000 requests per month by default and 100,000 with the corresponding account feature. Every response from these endpoints carriesX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(Unix seconds). When a quota is exhausted the endpoint returns429. Self-hosted operators can read usage atGET /accounts/{id}/ai/usage. - AI credits (hosted service). When the run uses the platform model (the default when you have not configured your own key), it is billed from the account's dollar credit balance, managed in the dashboard's AI console. If the balance is empty,
POST /ai/promptandPOST /ai/schedulereturn402 Payment Required. Runs on your own provider key (BYOK) do not touch the credit balance. Classify and the suggestions endpoints never consume credits.
Generate job configurations (POST /ai/prompt)
Request
{
"prompt": "Follow up 2 days after the demo",
"purposes": ["sales_follow_up"],
"events": ["demo_scheduled"],
"recipients": ["john@acme.com"],
"channels": ["email"],
"timezone": "America/New_York",
"locale": "en-US"
}
| Field | Rules |
|---|---|
prompt | Required. 1–160 characters after trimming. |
purposes, events, recipients, channels | Optional string arrays; at most 5 items, each at most 36 characters. Passed to the model as context. |
timezone | Optional IANA zone used to resolve relative phrases ("tomorrow", "9am"). Defaults to UTC. Invalid zones return 400. |
locale | Optional BCP-47 locale forwarded to the model so text fields such as subject are written in that language. Defaults to en. Any locale is accepted here (unlike /ai/prompt/classify). |
curl -X POST "https://api.scheduler0.com/api/v1/ai/prompt" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID" \
-d '{
"prompt": "Follow up 2 days after the demo",
"events": ["demo_scheduled"],
"recipients": ["john@acme.com"],
"channels": ["email"],
"timezone": "America/New_York"
}'
Response
{
"success": true,
"data": {
"providers": [
{
"provider": "openai",
"model": "gpt-4.1-mini",
"jobs": [
{
"kind": "FOLLOW_UP",
"purpose": "sales_follow_up",
"subject": "How was your demo?",
"nextRunAt": "2026-01-17T14:00:00-05:00",
"recurrence": "none",
"event": "demo_scheduled",
"delivery": "email",
"cronExpression": "",
"channel": "email",
"recipients": ["john@acme.com"],
"startDate": "2026-01-17T14:00:00-05:00",
"timezone": "America/New_York"
}
],
"inputTokens": 210,
"outputTokens": 95,
"totalTokens": 305,
"durationMs": 480
}
],
"classification": {
"text": "Follow up 2 days after the demo",
"decision": "allow",
"reason": "request_with_temporal_signal"
}
}
}
providers has one entry per model that ran (normally one). Each job proposal has:
| Field | Description |
|---|---|
kind | FOLLOW_UP (one-off, event-relative), REMINDER (recurring) or DIGEST (recurring aggregation) |
recurrence | none, every minute, every hour, every day, every week, every month or every year |
cronExpression | Cron spec for recurring proposals; empty for one-off proposals |
nextRunAt, startDate, endDate | RFC3339 with the offset of the requested timezone |
purpose, subject, event, delivery, channel, recipients, timezone, metadata | Model output; metadata is a free-form object |
classification is omitted when the classifier is not configured on the server (the request then proceeds without the guardrail).
To turn a proposal into a job, POST /jobs with spec: cronExpression for recurring proposals or startDate: nextRunAt (and no spec) for one-off proposals, plus your projectId, executorId and createdBy. See Jobs.
Guardrail rejection (422)
When the classifier decides the text is not a scheduling request, no model is invoked and the response is 422 with a structured body:
{
"success": false,
"data": {
"message": "prompt skipped (reject): informational_question_not_schedule_request",
"classification": {
"text": "What is Kubernetes?",
"decision": "reject",
"reason": "informational_question_not_schedule_request"
}
}
}
decision is allow, clarify or reject; both clarify and reject produce a 422.
Schedule from a prompt (POST /ai/schedule)
Runs the prompt pipeline, resolves or creates a project, picks an executor and creates the jobs synchronously. Returns 201 with everything it created.
{
"prompt": "Remind the sales team every Monday at 9am to review the pipeline",
"channels": ["email"],
"timezone": "America/New_York",
"projectId": 123,
"project": { "name": "Sales reminders", "description": "Weekly nudges" },
"executorId": 456,
"createdBy": "user@example.com"
}
| Field | Required | Description |
|---|---|---|
prompt | yes | Same rules as /ai/prompt |
createdBy | yes | Stamped on the created project and jobs |
purposes, events, recipients, channels, timezone, locale | no | Same hints as /ai/prompt; channels/purposes also inform executor matching |
projectId | no | Use an existing project. Takes precedence over project. |
project | no | {name, description}: reuse the project with that name or create it |
executorId | no | Pin an executor and skip matching |
If neither projectId nor project is given, a project is derived from the prompt.
Executor selection
executorIdsupplied → used after an ownership check (executorMatchedBy: "pinned"); an unknown id returns404.- The account has exactly one executor → used (
"only"). - Otherwise a model picks the executor whose
descriptionandtagsbest fit the prompt ("llm", withexecutorMatchReason).
If the account has no executors, no executor can be matched, or the prompt yields no schedulable jobs, the response is 409 and nothing is created. Give executors a meaningful description and tags (see Executors) or pin executorId.
Response
{
"success": true,
"data": {
"classification": { "text": "...", "decision": "allow", "reason": "request_with_temporal_signal" },
"project": { "id": 123, "name": "Sales reminders", "description": "Weekly nudges" },
"projectCreated": true,
"executor": { "id": 456, "name": "Email sender", "description": "Sends email", "tags": ["email"] },
"executorMatchedBy": "llm",
"executorMatchReason": "matches the email channel",
"jobs": [
{ "id": 789, "projectId": 123, "executorId": 456, "spec": "0 0 9 * * 1", "timezone": "America/New_York", "status": "active" }
],
"provider": "openai",
"model": "gpt-4.1-mini"
}
}
The project and the jobs are separate writes; if job creation fails after a project was created, the project remains.
curl -X POST "https://api.scheduler0.com/api/v1/ai/schedule" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID" \
-d '{
"prompt": "Remind the sales team every Monday at 9am to review the pipeline",
"channels": ["email"],
"timezone": "America/New_York",
"createdBy": "user@example.com"
}'
Classify a prompt (POST /ai/prompt/classify)
Runs only the intent guardrail. No generation model is invoked and no credits are spent; it counts against the classify quota. Useful for inline validation before spending a prompt request.
curl -X POST "https://api.scheduler0.com/api/v1/ai/prompt/classify" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID" \
-d '{"prompt": "What is Kubernetes?"}'
{
"success": true,
"data": {
"classification": {
"text": "What is Kubernetes?",
"decision": "reject",
"reason": "informational_question_not_schedule_request"
}
}
}
| Field | Rules |
|---|---|
prompt | Required, 1–160 characters |
locale | Optional. The classifier is English-only: values not starting with en return 400. |
Returns 503 if the classifier is not configured on the server.
Recommending send times (POST /ai/suggestions/time)
Recommends future send times for a message given sender and recipient time zones, working and quiet hours, weekends, holidays, calendar busy intervals and multi-recipient coverage rules. The engine is deterministic time-zone math: no model is invoked, no credits or quota are consumed, and nothing is sent or scheduled. Field names are snake_case.
Only recipients is required; each recipient needs an IANA timezone.
curl -X POST "https://api.scheduler0.com/api/v1/ai/suggestions/time" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID" \
-d '{
"sender": { "timezone": "America/New_York" },
"recipients": [
{ "id": "u1", "timezone": "Europe/London" },
{ "id": "u2", "timezone": "Asia/Tokyo" }
],
"message": { "channel": "email", "priority": "normal" },
"constraints": { "working_hours_only": true, "avoid_weekends": true },
"group_policy": { "strategy": "overlap", "minimum_recipient_coverage": 0.6 },
"options": { "suggestion_count": 3, "search_horizon_days": 7 }
}'
| Block | Purpose |
|---|---|
sender, recipients[] | Participants: id, timezone, optional working_hours / quiet_hours |
message | Channel, priority and other timing-relevant metadata |
constraints | Hard rules; violating candidates are rejected |
preferences | Soft windows that affect scoring only |
group_policy | strategy and minimum_recipient_coverage for multiple recipients |
options | reference_time, suggestion_count, candidate_interval_minutes, search_horizon_days, evaluate_send_now, include_score_breakdown, include_rejected_summary, diversify_suggestions, minimum_suggestion_spacing_minutes, locale (English only; other values return 400) |
holiday_policy | dates to avoid (country/region lookups are not implemented yet) |
availability | Per-participant busy intervals |
The response contains request_id, reference_time, policy, engine, search (candidate window and counts), suggestions (best first, each with send_at, score, rank, reason, recipient_local_times, coverage), optional rejected_summary, no_suggestion (when nothing qualified), send_now (when evaluate_send_now was set) and warnings. Invalid input returns 400 with {code, message, field}.
Prompt request log (GET /ai/prompt-requests)
Every POST /ai/prompt run and the prompt phase of POST /ai/schedule is persisted with provider, model, token counts, estimated cost, status and timing.
curl "https://api.scheduler0.com/api/v1/ai/prompt-requests?provider=openai&status=success&limit=25" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID"
| Parameter | Description |
|---|---|
limit | Default 25, clamped to 100 |
offset | Default 0 |
provider, model | Filters |
status | e.g. success, failed, skipped_intent |
search | Text search over the prompt |
start, end | RFC3339 bounds |
order | ASC; anything else sorts descending |
{
"success": true,
"data": {
"requests": [
{
"id": 1,
"account_id": 123,
"prompt": "Remind me every Monday",
"provider": "openai",
"model": "gpt-4.1-mini",
"output": "{...}",
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"duration_ms": 420,
"estimated_cost_usd": 0.0001,
"status": "success",
"date_created": "2026-01-15T14:00:00Z"
}
],
"total": 1,
"limit": 25,
"offset": 0
}
}
AI provider settings (bring your own key)
By default prompts run on the platform model (Claude Sonnet 4.5 on AWS Bedrock, billed from AI credits on the hosted service). You can instead store your own provider keys and an ordered list of active models: the first is the primary, the rest are fallbacks tried in order when the primary fails. Providers: platform, openai, anthropic, bedrock, openrouter.
Model catalog (GET /ai/models)
Only models in this catalog can be saved.
curl "https://api.scheduler0.com/api/v1/ai/models" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID"
{
"success": true,
"data": {
"platform": [
{ "id": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", "display_name": "Scheduler0 (Claude Sonnet 4.5)", "default": true }
],
"openai": [
{ "id": "gpt-4.1-mini", "display_name": "GPT-4.1 Mini", "default": true },
{ "id": "gpt-4.1", "display_name": "GPT-4.1" }
],
"anthropic": [
{ "id": "claude-sonnet-4-5", "display_name": "Claude Sonnet 4.5" }
],
"bedrock": [
{ "id": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", "display_name": "Claude Sonnet 4.5 (Bedrock)" }
],
"openrouter": [
{ "id": "openai/gpt-4.1-mini", "display_name": "GPT-4.1 Mini (OpenRouter)", "default": true }
]
}
}
The lists above are abbreviated; call the endpoint for the current catalog. OpenRouter models are limited to those that support strict json_schema structured output.
Read settings (GET /ai/settings)
Keys are never returned. A configured key appears as "•"; an unconfigured one is omitted. If nothing has been saved you get an object with just account_id.
{
"success": true,
"data": {
"account_id": 123,
"active_models": [
{ "provider": "openai", "model": "gpt-4.1-mini" },
{ "provider": "anthropic", "model": "claude-sonnet-4-5" }
],
"openai_api_key": "•",
"anthropic_api_key": "•",
"date_created": "2026-01-15T10:30:00Z"
}
}
Save settings (PUT /ai/settings)
Upserts the account's settings. account_id is taken from the authenticated request.
curl -X PUT "https://api.scheduler0.com/api/v1/ai/settings" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Secret-Key: YOUR_SECRET_KEY" \
-H "X-Account-ID: YOUR_ACCOUNT_ID" \
-d '{
"active_models": [
{ "provider": "openai", "model": "gpt-4.1-mini" },
{ "provider": "anthropic", "model": "claude-sonnet-4-5" }
],
"openai_api_key": "sk-...",
"anthropic_api_key": "sk-ant-..."
}'
| Field | Description |
|---|---|
active_models[] | Ordered {provider, model} pairs. Both values are required; model must be in the catalog for that provider. |
openai_api_key, anthropic_api_key, openrouter_api_key | Provider keys. Leave a key out (or empty) to keep the stored value. |
bedrock_access_key_id, bedrock_secret_key, bedrock_region | AWS credentials for Bedrock. Same keep-if-empty rule. |
Validation returns 422 when a model is not approved, or when an active_models entry names a provider (other than platform) that has no key stored or supplied in the same request. Malformed JSON also returns 422. Success returns { "message": "..." }.
Keys are encrypted at rest and only decrypted while executing a prompt.
Related
- Jobs — creating jobs from the proposals
- Executors —
description/tagsused for matching - Conversation Suggestions
- Account Management — reading quota usage (self-hosted)