Skip to content

Setup recurring

Configure a recurring cost-reduction agent through a progressive Q-and-A wizard. Each call merges the latest answer into a session (keyed on session_id) and either asks the next missing question or, once all answers are in, emits policy.yaml plus the chosen scheduler manifest with step-by-step apply instructions.

The scheduler manifest is one of:

  • Kubernetes CronJob
  • GitHub Actions workflow
  • crontab + wrapper script
  • EventBridge Scheduler + CodeBuild (CloudFormation template, S3 config plane)

Four goals: target_percent or budget_gb_monthly for the mute-list tick; budget_usd_monthly, the Datadog Standard budget held month to date; rows_per_hour, the ClickHouse rows run that holds the rows written into a logs table at a rate an hour, read from system.part_log; hot_tier_gb, the Elasticsearch hot-tier run that holds the hot nodes' index store under a size. The last three run hourly on k8s_cron or github_actions.

Example

You

set up a weekly 30% savings tick

Log10x

Wizard Q1: which services should the policy target?

Pass target_services: [] for all, or a named list.

Re-invoke with session_id: "recurring-lx8a-9k2pq3".

More to ask

  • "recurring policy: payments and checkout, 40%, daily 3am UTC"
  • "emit a k8s CronJob that trims 25% off cart-svc weekly"
  • "schedule a GitHub Actions tick every 6h against acme/log10x-config"
  • "AWS-native tick for our Lambda estate, no cluster, no GitHub"

Prerequisites

Reporter deployed (metrics drive the per-pattern planner that the tick invokes). A config plane for policy.yaml, a gitops repo, local path, or S3 prefix, and credentials for the chosen scheduler (kubectl access for k8s_cron, repo write for github_actions, host shell for crontab, CloudFormation deploy rights for eventbridge).

For hot_tier_gb: the Receiver installed with Advise install and log_destination: "elasticsearch", which counts bytes and stamps utf8Size and tenx_hash on every record it returns. The Elasticsearch frozen-tier recipe from Advise retriever applied: it writes the tier_down index on warm when the cluster has a warm node (required for hot_tier_gb: auto) and moves kept lines to frozen after 7 days on hot, which the run reads from the tenx-app ILM policy; a policy of the account's own on tenx-app needs a phase after hot, or hot_retention_hours set in policy.yaml. A Secret holding LOG10X_ES_URL and LOG10X_ES_API_KEY (default name log10x-elasticsearch), or the same two as repository secrets on github_actions.

Schema and samples

Input example

Representative call (synthetic, not captured from the live demo env).

{
  "target_services": ["payment", "checkout"],
  "target_percent": 30,
  "schedule": "daily-03utc",
  "scheduler": "k8s_cron",
  "config_plane": "https://github.com/acme/log10x-config",
  "confirm": true
}
Input schema

Agent-facing JSON Schema (the canonical shape the MCP server publishes via tools/list):

{
  "type": "object",
  "properties": {
    "session_id": {
      "type": "string",
      "description": "Wizard session handle. Omit on the first call: a new session is minted and returned. Pass it back unchanged on every subsequent call."
    },
    "target_services": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "description": "Services the policy targets. Empty array = all services. Use service names from `log10x_services`. The wizard asks if omitted."
    },
    "target_percent": {
      "type": "integer",
      "minimum": 1,
      "maximum": 95,
      "description": "Desired savings target, as a percentage of current log volume (1-95). Default: 30. The CLI's per-pattern planner works backward from this target."
    },
    "budget_gb_monthly": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "VOLUME BUDGET: keep monthly ingest for the targeted services at or under this many GB/mo. A standing line, not a one-shot cut: each tick scales the budget to its lookback window and cuts only the overage: already under budget means every pattern passes. Replaces target_percent in policy.yaml (mutually exclusive). Dollar budgets are a one-shot answer via log10x_estimate_savings (budget_usd_monthly); the recurring tick is byte-denominated."
    },
    "budget_usd_monthly": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "DATADOG STANDARD BUDGET: dollars a month of Standard indexing for the whole account, held month to date. Each run reads what Standard counted this month (Datadog's estimated usage metric), subtracts lines that do not pass the Receiver, gives the Receiver's services the rest for the hours left, and writes caps.csv, actions.csv and totals.csv (with a `*` row for every other service), counted in events. Runs on scheduler k8s_cron (writes the policy ConfigMap in the cluster, discovers node counts) or github_actions (commits the files for the engine's gitops pull, node counts from `nodes_by_container`). Replaces target_percent and budget_gb_monthly. The Receiver must be installed with `regulator_unit: \"events\"` on engine 1.1.146 or later."
    },
    "standard_indexes": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "description": "With budget_usd_monthly: the Datadog index names that bill as Standard. Omit to count every index."
    },
    "standard_rate_per_million": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "With budget_usd_monthly: the account's Standard indexing rate, dollars per million events. Default: list, $1.70."
    },
    "nodes_by_container": {
      "type": "object",
      "additionalProperties": {
        "type": "integer",
        "exclusiveMinimum": 0
      },
      "description": "With budget_usd_monthly, rows_per_hour or hot_tier_gb on github_actions: the nodes each service's pods run on. The run divides every limit by them."
    },
    "service_budgets": {
      "type": "object",
      "additionalProperties": {
        "anyOf": [
          {
            "type": "number",
            "exclusiveMinimum": 0
          },
          {
            "type": "object",
            "properties": {
              "usd": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "datadog_service": {
                "type": "string",
                "minLength": 1
              }
            },
            "required": [
              "usd"
            ],
            "additionalProperties": false
          }
        ]
      },
      "propertyNames": {
        "minLength": 1
      },
      "description": "With budget_usd_monthly: dollars a month of Standard for a named service (the container), e.g. {\"payments\": 4000}, or {\"payments\": {\"usd\": 4000, \"datadog_service\": \"payments-api\"}} when Datadog tags it with another service name. Each run holds that service month to date against its own number, from Datadog's usage count under that service tag, and holds the rest of the account to budget_usd_monthly minus their sum. The sum must be under budget_usd_monthly, and a service in exceptions cannot have one."
    },
    "keep_count": {
      "type": "integer",
      "minimum": 0,
      "description": "With budget_usd_monthly, rows_per_hour or hot_tier_gb: error lines over a limit that pass per statement per node per window. Default 50."
    },
    "datadog_secret_name": {
      "type": "string",
      "description": "With budget_usd_monthly on k8s_cron: the Secret holding DD_PAT (or DD_API_KEY and DD_APP_KEY) and optional DD_SITE. Default: log10x-datadog."
    },
    "rows_per_hour": {
      "anyOf": [
        {
          "type": "number",
          "exclusiveMinimum": 0
        },
        {
          "type": "string",
          "const": "auto"
        }
      ],
      "description": "CLICKHOUSE ROWS RATE: rows an hour written into the logs table, held at this figure, or `auto` for the meter's last complete hour at the first run. Each run reads NewPart rows from system.part_log and each statement's lines from the Receiver's Prometheus counters, and rewrites the limits and totals in lines so the excess goes to the account's own bucket before ClickHouse receives it. Replaces target_percent, budget_gb_monthly and budget_usd_monthly (k8s_cron or github_actions). The Receiver must be installed with log_destination clickhouse (counting events)."
    },
    "clickhouse_table": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_]+\\.[A-Za-z0-9_]+$",
      "description": "With rows_per_hour: the logs table the run meters, as database.table. Default default.otel_logs."
    },
    "clickhouse_secret_name": {
      "type": "string",
      "description": "With rows_per_hour on k8s_cron: the Secret holding CLICKHOUSE_URL, CLICKHOUSE_API_KEY (or CLICKHOUSE_USER and CLICKHOUSE_PASSWORD) and LOG10X_CUSTOMER_METRICS_URL. Default: log10x-clickhouse."
    },
    "hot_tier_gb": {
      "anyOf": [
        {
          "type": "number",
          "exclusiveMinimum": 0
        },
        {
          "type": "string",
          "const": "auto"
        }
      ],
      "description": "ELASTICSEARCH HOT TIER: GB of index store on the hot nodes to hold under, or `auto` for 80% of the hot nodes' disk, refused while the tier_down write index sits on hot (the recipe from log10x_advise_retriever writes it on warm when the cluster has a warm node; otherwise give a size). Each run reads the hot nodes' store and the Receiver's indices still on hot, and rewrites each statement's byte limit so lines over it go to the frozen tier and the hot tier settles at the size under steady load. Replaces target_percent, budget_gb_monthly, budget_usd_monthly and rows_per_hour (k8s_cron or github_actions). The Receiver is installed with log10x_advise_install and log_destination elasticsearch (it counts bytes and stamps utf8Size and tenx_hash on each record), and its indices follow the Elasticsearch frozen-tier recipe from log10x_advise_retriever."
    },
    "elasticsearch_secret_name": {
      "type": "string",
      "description": "With hot_tier_gb on k8s_cron: the Secret holding LOG10X_ES_URL and LOG10X_ES_API_KEY (an encoded API key). Default: log10x-elasticsearch."
    },
    "schedule": {
      "anyOf": [
        {
          "type": "string",
          "enum": [
            "hourly",
            "daily-03utc",
            "every-6h",
            "every-12h",
            "every-24h-localtz"
          ]
        },
        {
          "type": "string",
          "pattern": "^[\\d*,\\-\\/\\s]+$"
        }
      ],
      "description": "Tick schedule. Presets: hourly (the default for budget_usd_monthly, rows_per_hour and hot_tier_gb), daily-03utc (the default otherwise), every-6h, every-12h, every-24h-localtz. Or pass a raw 5-field cron expression (e.g. \"0 5 * * 1\")."
    },
    "scheduler": {
      "type": "string",
      "enum": [
        "k8s_cron",
        "github_actions",
        "crontab",
        "eventbridge",
        "container_apps_job"
      ],
      "description": "Where the recurring tick runs. k8s_cron = Kubernetes CronJob (default when kubectl reachable), github_actions = GHA workflow, crontab = crontab + wrapper script, eventbridge = AWS-native (EventBridge Scheduler + CodeBuild, S3 config plane: no cluster, no GitHub; the fit for AWS serverless estates), container_apps_job = Azure-native (Container Apps scheduled Job, git config plane; the fit for Azure serverless estates)."
    },
    "config_plane": {
      "type": "string",
      "minLength": 1,
      "description": "Where the recurring CLI reads policy.yaml and writes updated cap CSVs: a gitops repo URL (e.g. https://github.com/acme/log10x-config), a local path, or (required when scheduler=eventbridge) an S3 prefix (e.g. s3://acme-logs/log10x-config)."
    },
    "exceptions": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "description": "Service names the policy must never touch (audit / regulatory / executive). Optional: defaults to empty. Pass [] to explicitly clear all exceptions."
    },
    "min_delta_pp": {
      "type": "integer",
      "minimum": 0,
      "maximum": 50,
      "description": "Minimum change in savings percentage points required before the tick commits a new CSV. Default: 2. Set to 0 to commit on every tick."
    },
    "env_id": {
      "type": "string",
      "description": "Log10x environment ID. Auto-detected from LOG10X_ENV_ID when absent."
    },
    "namespace": {
      "type": "string",
      "description": "Kubernetes namespace for the CronJob. Default: log10x. Only used when scheduler=k8s_cron."
    },
    "secret_name": {
      "type": "string",
      "description": "Name of the Kubernetes Secret holding LOG10X_API_KEY. Default: log10x-secret. Only used when scheduler=k8s_cron."
    },
    "confirm": {
      "type": "boolean",
      "description": "Set to true to confirm the configuration and emit the artifacts. The wizard asks for confirmation interactively when omitted."
    }
  },
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Source: src/tools/setup-recurring.ts.

Output example

Representative envelope (synthetic, not captured from the live demo env). view: "summary" returns the full StructuredOutput with typed data. Long YAML bodies trimmed for readability; the real call returns them in full.

Headline (the 1-line agent-facing answer):

Recurring policy emitted: log10x-cronjob.yaml + policy.yaml ready

{
  "schema_version": "1.0",
  "schema_epoch": "2026-05-25",
  "tool": "log10x_setup_recurring",
  "view": "summary",
  "summary": {
    "headline": "Recurring policy emitted: log10x-cronjob.yaml + policy.yaml ready"
  },
  "data": {
    "mode": "emit",
    "ok": true,
    "session_id": "recurring-lx8a-9k2pq3",
    "policy_yaml": "target_services:\n  - payment\n  - checkout\ntarget_percent: 30\nschedule: daily-03utc\nmin_delta_pp: 2\n...",
    "scheduler_manifest": "apiVersion: batch/v1\nkind: CronJob\nmetadata:\n  name: log10x-recurring\n  namespace: log10x\nspec:\n  schedule: \"0 3 * * *\"\n...",
    "scheduler_manifest_filename": "log10x-cronjob.yaml",
    "apply_instructions": "### Apply instructions: Kubernetes CronJob\n\n1. Commit `policy.yaml` to the root of your gitops repo...",
    "markdown": "## Recurring cost-reduction policy: artifacts ready\n\n| Field | Value |\n|---|---|\n| Services | payment, checkout |\n...",
    "human_summary": "Recurring policy emitted for 2 services targeting 30% savings via k8s_cron. Artifacts: policy.yaml + log10x-cronjob.yaml. Apply instructions are in data.apply_instructions. Run log10x_commitment_report after the first week to verify realized savings."
  },
  "actions": [
    {
      "tool": "log10x_doctor",
      "args": {},
      "reason": "confirm the environment is healthy before the first tick runs",
      "role": "optional-followup"
    }
  ],
  "truncated": false,
  "warnings": []
}
Output schema

The data block inside the StructuredOutput envelope. The shape is a discriminated union on mode: next_question while the wizard is still gathering answers, emit once all questions are answered.

type ToolData =
  | {
      mode: 'next_question';
      ok: false;
      session_id: string;
      question_id:
        | 'target_services'
        | 'target_percent'
        | 'schedule'
        | 'scheduler'
        | 'config_plane'
        | 'exceptions'
        | 'confirm';
      markdown: string;
      human_summary: string;
    }
  | {
      mode: 'emit';
      ok: true;
      session_id: string;
      policy_yaml: string;
      scheduler_manifest: string;
      scheduler_manifest_filename: string;
      apply_instructions: string;
      crontab_wrapper_script?: string;
      markdown: string;
      human_summary: string;
    };

Envelope-level fields the agent should also read: summary.headline (1-line answer), actions[] (next-call chain hints as {tool, args, reason}: required-next while questions remain, optional-followup pointing at log10x_doctor after emit), truncated: boolean, images[] (PNG attachments where applicable), schema_epoch (engine-ID stability boundary).