Scheduled AI jobs with your own Claude, OpenAI or Grok key

Run a prompt on a schedule and send the answer on: a morning summary to Discord, a triage of new tickets to Slack, or JSON that the next step posts to your API. The ai_generate step calls Anthropic, OpenAI or xAI with your own API key, so you pick the model and usage is billed to your AI account. It can also call your APIs, and the tools of remote MCP servers, while it works.

Example: a daily summary to Discord

The reply is text, so the Discord step sends all of it with {{ $ }}. max_chars is Discord's 2000-character limit: the model is told, and a reply that runs over is sent back once to be shortened.

json
{
  "name": "Open PRs, summarized for #eng",
  "cron": { "expression": "0 9 * * 1-5", "time_zone": "Europe/Berlin" },
  "nodes": [
    { "id": "prs", "request_template": { "template_id": "<id of github: List pull requests>", "credential_id": "YOUR_GITHUB_CREDENTIAL_ID", "values": { "owner": "acme", "repo": "web", "state": "open" } } },
    { "id": "sum", "ai_generate": {
        "provider": "anthropic", "model": "claude-opus-5-5", "credential_id": "YOUR_ANTHROPIC_KEY_ID", "effort": "low",
        "system": "You write the morning update for an engineering team. Format it as Discord markdown: **bold**, *italic*, `code`, and - for lists. No tables.",
        "prompt": "Summarize these open pull requests. Put anything waiting on review for more than two days first: {{ $ }}",
        "text": { "max_chars": 2000 } } },
    { "id": "post", "request_template": { "template_id": "<id of discord: Send message>", "values": { "webhook_url": "https://discord.com/api/webhooks/123456789012345678/abcDEF_ghi-JKL", "content": "{{ $ }}" } } }
  ]
}

Example: JSON for the next request

With json output, the reply is held to a JSON Schema through the provider's structured outputs, so it always parses, and the next step reads its fields. Here the model turns yesterday's failed Stripe payments into a GitHub issue.

json
{
  "name": "Failed payments to an issue",
  "cron": { "expression": "0 8 * * *", "time_zone": "America/New_York" },
  "nodes": [
    { "id": "subs", "request_template": { "template_id": "<id of stripe: List subscriptions>", "credential_id": "YOUR_STRIPE_CREDENTIAL_ID", "values": { "status": "past_due" } } },
    { "id": "draft", "ai_generate": {
        "provider": "openai", "model": "YOUR_OPENAI_MODEL", "credential_id": "YOUR_OPENAI_KEY_ID",
        "prompt": "Write a GitHub issue for the finance team about these past-due subscriptions: {{ $.data }}",
        "json": { "schema": "{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"body\":{\"type\":\"string\"},\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}}}" } } },
    { "id": "issue", "request": {
        "method": "POST", "endpoint": "https://api.github.com/repos/acme/finance/issues", "credential_id": "YOUR_GITHUB_CREDENTIAL_ID",
        "body": "{ \"title\": \"{{ $.title }}\", \"body\": \"{{ $.body }}\", \"labels\": {{ $.labels }} }" } }
  ]
}

The server makes the schema strict before sending it: every property is required and no others are allowed. For a field that may be empty, use a type like ["string", "null"].

Example: an agent with tools

tools are HTTP calls the model may make. Each one is a request, a request template or an email, with $ bound to the arguments the model chose, so {{ $.email }} below is the email it asks about. The response goes back to the model, not to the next step. A tool that fails (a 404, a timeout) is reported to the model, which carries on.

json
{ "id": "triage", "ai_generate": {
    "provider": "xai", "model": "YOUR_GROK_MODEL", "credential_id": "YOUR_XAI_KEY_ID",
    "prompt": "For each new ticket, look up the customer and say whether they're on a paid plan. Tickets: {{ $.results }}",
    "text": { "max_chars": 40000 },
    "tools": [{
      "name": "lookup_customer",
      "description": "Find a customer by email. Returns their plan and signup date.",
      "parameters": "{\"type\":\"object\",\"properties\":{\"email\":{\"type\":\"string\",\"description\":\"The customer's email\"}}}",
      "step": { "id": "t1", "request": { "method": "GET", "endpoint": "https://api.acme.com/customers?email={{ $.email }}", "credential_id": "YOUR_ACME_CREDENTIAL_ID" } }
    }] } }

Example: tools from an MCP server

mcp_servers gives the model the tools of a remote MCP server (Streamable HTTP), such as Financial Modeling Prep's market data. Each run starts a session, lists the server's tools and offers them as <name>_<tool>. The server's credential (a bearer token, a header or a URL parameter) is sent with every request. tools limits the model to the ones you name; omit it to offer every tool the server lists.

json
{ "id": "brief", "ai_generate": {
    "provider": "openai", "model": "YOUR_OPENAI_MODEL", "credential_id": "YOUR_OPENAI_KEY_ID",
    "prompt": "Is anything on my watchlist moving more than 3% today? {{ $.symbols }}",
    "text": { "max_chars": 2000 },
    "mcp_servers": [{ "name": "fmp", "url": "https://financialmodelingprep.com/mcp", "credential_id": "YOUR_FMP_CREDENTIAL_ID", "tools": ["quote", "news"] }] } }

POST /steps/mcp-tools with {"server": {…}} lists a server's tools, the same list the editor's Load tools shows. MCP tool calls count toward the 100, and a tool that reports an error goes back to the model like a failed HTTP tool.

Set up

  1. Create an API key with your provider: the Claude Console for Claude, platform.openai.com for OpenAI, or console.x.ai for Grok.
  2. Save it as a credential of kind bearer_token (the Anthropic, OpenAI or xAI partner in the app). It's encrypted at rest and never shown again.
  3. Add an AI step in the job editor. It asks what the next step needs (a Discord message, or JSON for a request) and sets up the output to match. Then write the prompt, add any tools, and use Try it to run the step against the previous step's sample before you save.

Agents can do the same with POST /steps/test: send {"node": …, "input": "<the step's $ as JSON>"} and get back the step's result and output, with nothing recorded.

Limits

  • The whole step has 15 minutes, tool calls included, and each tool call has 30 seconds. Lower effort answers faster.
  • effort is low, medium or high, or empty for the model's default. OpenAI and Grok also take none, which turns reasoning off. Newer OpenAI models, such as gpt-6-luna, need none to call tools.
  • The editor lists the models your key can use, as the provider reports them: Anthropic's active models, OpenAI's chat models and xAI's text models. You can also type any model id. POST /steps/ai-models with {"provider": "AI_PROVIDER_OPENAI", "credential_id": "…"} returns the same list.
  • Up to 50 tools and 10 MCP servers (64 tools each), and 100 tool calls per run.
  • A tool’s response is cut off at 256 KiB before it goes back to the model.
  • Each run records the tool calls with their status codes, and the tokens the provider reported.

Generate with Claude, OpenAI or Grok: ai_generate

Sends Your prompt to the provider’s API with your own key (Anthropic Messages, or OpenAI-style chat completions for OpenAI and xAI), and makes the tool calls the model asks for. Output for the next step: The reply: a string with text output (use {{ $ }}), or the parsed object with json output (use {{ $.field }}).

Field Type Notes
provider string, required anthropic, openai or xai (in the proto, AI_PROVIDER_ANTHROPIC and so on).
model string, required The provider’s model id, such as claude-opus-5-5.
credential_id string, required A bearer_token credential holding the provider’s API key, sent as x-api-key to Anthropic and as Authorization: Bearer to OpenAI and xAI. An api_key_header also works for Anthropic. Usage is billed to that account.
system string Instructions: who the reply is for, its tone and format. The rule for the output is added for you. Placeholders allowed.
prompt string, required The task and its data, such as Summarize: {{ $.results }}. Lists and objects are filled in as JSON. Placeholders allowed.
text object Set this or json. The reply is plain text. max_chars is the longest reply the next step takes, such as 2000 for Discord: the model is told, asked once to shorten a longer reply, and the step fails if it is still over.
json object Set this or text. The reply is JSON matching schema, a JSON Schema object sent as the provider’s structured output, so the next step can use {{ $.field }}. Every property is required; use a type like ["string", "null"] for one that may be empty.
tools object[] Up to 50 HTTP calls the model may make, 100 calls in total per run. Each has a name, a description for the model, parameters (a JSON Schema string for its arguments) and a step: a request or built-in step where $ is the model’s arguments. A failed call is reported to the model, which carries on.
max_tokens integer Per model call, reasoning included. Up to 128,000 (and the model’s own limit); omit for 16,000.
effort string low, medium or high; omit for the model’s default. Lower is faster.
sample_response string Editor only: an example reply for previewing later steps. Never sent.
mcp_servers object[] Up to 10 remote MCP servers (Streamable HTTP) whose tools the model may call, such as Financial Modeling Prep’s at https://financialmodelingprep.com/mcp; GET /mcp-server-templates lists the ones cronservices knows. Each run lists the server’s tools and offers them as <name>_<tool>; the calls count toward the 100.