Skip to main content

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/prompt returns proposed job configurations. You decide what to do with them, typically by calling POST /jobs.
  • POST /ai/schedule runs 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​

MethodPathScopeNotes
POST/ai/promptexecuteGenerate job configurations
POST/ai/scheduleexecuteGenerate and create jobs → 201
POST/ai/prompt/classifyexecuteIntent guardrail only
POST/ai/suggestions/analyzeexecuteSee Conversation Suggestions
POST/ai/suggestions/timeexecuteRecommend send times
GET/ai/prompt-requestsreadAudit log of prompt runs
GET/ai/modelsreadApproved model catalog
GET / PUT/ai/settingsread / writeBring-your-own-key configuration

Quotas, credits and rate-limit headers​

Two independent limits apply:

  • Monthly request quotas. POST /ai/prompt and POST /ai/schedule draw from the account's prompt quota; POST /ai/prompt/classify and POST /ai/suggestions/analyze draw 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 carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). When a quota is exhausted the endpoint returns 429. Self-hosted operators can read usage at GET /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/prompt and POST /ai/schedule return 402 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"
}
FieldRules
promptRequired. 1–160 characters after trimming.
purposes, events, recipients, channelsOptional string arrays; at most 5 items, each at most 36 characters. Passed to the model as context.
timezoneOptional IANA zone used to resolve relative phrases ("tomorrow", "9am"). Defaults to UTC. Invalid zones return 400.
localeOptional 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:

FieldDescription
kindFOLLOW_UP (one-off, event-relative), REMINDER (recurring) or DIGEST (recurring aggregation)
recurrencenone, every minute, every hour, every day, every week, every month or every year
cronExpressionCron spec for recurring proposals; empty for one-off proposals
nextRunAt, startDate, endDateRFC3339 with the offset of the requested timezone
purpose, subject, event, delivery, channel, recipients, timezone, metadataModel 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"
}
FieldRequiredDescription
promptyesSame rules as /ai/prompt
createdByyesStamped on the created project and jobs
purposes, events, recipients, channels, timezone, localenoSame hints as /ai/prompt; channels/purposes also inform executor matching
projectIdnoUse an existing project. Takes precedence over project.
projectno{name, description}: reuse the project with that name or create it
executorIdnoPin an executor and skip matching

If neither projectId nor project is given, a project is derived from the prompt.

Executor selection​

  1. executorId supplied → used after an ownership check (executorMatchedBy: "pinned"); an unknown id returns 404.
  2. The account has exactly one executor → used ("only").
  3. Otherwise a model picks the executor whose description and tags best fit the prompt ("llm", with executorMatchReason).

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"
}
}
}
FieldRules
promptRequired, 1–160 characters
localeOptional. 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 }
}'
BlockPurpose
sender, recipients[]Participants: id, timezone, optional working_hours / quiet_hours
messageChannel, priority and other timing-relevant metadata
constraintsHard rules; violating candidates are rejected
preferencesSoft windows that affect scoring only
group_policystrategy and minimum_recipient_coverage for multiple recipients
optionsreference_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_policydates to avoid (country/region lookups are not implemented yet)
availabilityPer-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"
ParameterDescription
limitDefault 25, clamped to 100
offsetDefault 0
provider, modelFilters
statuse.g. success, failed, skipped_intent
searchText search over the prompt
start, endRFC3339 bounds
orderASC; 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-..."
}'
FieldDescription
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_keyProvider keys. Leave a key out (or empty) to keep the stored value.
bedrock_access_key_id, bedrock_secret_key, bedrock_regionAWS 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.