Elastic ML Anomaly Detection
Single skill covering all anomaly detection work against Kibana Agent Builder MCP at
{KIBANA_URL}/api/agent_builder/mcp. Use the Mode Selector below to pick the right approach for the user's question
— modes share the same tool surface and concepts.
Platform
- Read path: ES|QL against
.ml-anomalies-*, .ml-config, .ml-notifications-*, .ml-annotations-*
- Always-available:
platform.core.execute_esql (plus additional platform tools for search, index mapping, and
documentation — see scripts/agent_builder_constants.json)
- ML API spec (if available):
.kibana_ai_openapi_spec_elasticsearch — see
references/anomaly-detection-openapi-spec-discover.md for
discovery pattern.
- Run
ad_validate_ml_tool_permissions first when tools return empty/misleading results — missing privileges are
the most common cause of false negatives. Full permissions matrix:
references/permissions-matrix.md.
Mode Selector
| User intent |
Mode |
| "What broke?" / RCA / cross-job / blast radius / influencers / log categories |
Investigate |
| "Why score high/low?" / renormalization / model bounds / forecasts |
Explain |
| Missing docs / memory limit / datafeed stopped / CCS / lifecycle / calendars |
Troubleshoot |
| Create a job / configure a datafeed / start analysis / retrieve results |
Manage |
| Security framing (attack chains, MITRE, exfil) |
Investigate + references/security-anomaly-expert.md |
| Observability/SRE framing (degradation, capacity, deployment regression) |
Investigate + references/observability-anomaly-expert.md |
When a question spans modes: Investigate → Explain → Troubleshoot. Don't blend mode logic — finish one before moving
on.
Score Quick Reference
record_score bands: >75 critical · 50–75 warning · 25–50 minor · <25 informational
multi_bucket_impact ≥ 3 → sustained shift (not a transient spike)
initial_record_score >> record_score → renormalization (model saw worse anomalies later)
actual << typical with count/low_count/low_mean → absence/outage, not just low value
- Low scores across many jobs > one high score — composite cross-job signal often beats single-detector severity
Full score definitions, renormalization mechanics, and anomaly_score_explanation components:
references/score-reference.md.
Core concepts
Treat .ml-anomalies-* as three layers, accessed via result_type:
bucket — bucket-level unusualness per bucket_span. anomaly_score is the aggregate across all detectors.
record — finest-grained rows with actual vs typical, probability, record_score,
anomaly_score_explanation.
influencer — entity contributions ranked within a bucket (influencer_score).
Read scores this way:
anomaly_score / record_score = current normalized values (move as the model sees new extremes).
initial_anomaly_score / initial_record_score = immutable snapshots from detection time.
- Compare
actual to typical; use probability for raw likelihood.
- Map entities via
partition_field_value / by_field_value / over_field_value.
- Read
multi_bucket_impact (-5 to +5) to separate single-bucket spikes from sustained trends.
Mode: Investigate — RCA
When: "what broke?", "which entity caused this?", cross-job correlation, blast radius, attack/cascade chains.
Tool chain
| Phase |
Tools |
| Discovery |
ad_get_available_metadata, ad_get_jobs, ad_discover_related_jobs, ad_discover_jobs_by_datafeed_index |
| Timeline / scope |
ad_query_anomaly_timeline |
| Cross-job / entities |
ad_rca_cross_job_entity_match, ad_rca_multi_job_entities, ad_rca_entity_profile |
| Records / influencers |
ad_query_anomaly_records, ad_query_influencers |
| RCA depth |
ad_rca_detector_fingerprint, ad_rca_correlation, ad_rca_blast_radius, ad_rca_score_reassessment |
| Evidence / categories |
ad_get_job_datafeed_config, ad_rca_source_evidence, ad_get_categories, ad_search_log_category_examples |
Protocol
Follow the 14-step sequence in references/protocols/investigation.md. High
level: ad_get_available_metadata → pair ad_discover_jobs_by_datafeed_index with ad_discover_related_jobs →
ad_query_anomaly_timeline → rank with ad_rca_multi_job_entities (min_job_count=2) → ad_rca_detector_fingerprint
→ drill with ad_query_anomaly_records + ad_query_influencers (low min_score=25) → profile with
ad_rca_entity_profile → order with ad_rca_correlation → confirm with ad_rca_source_evidence. When
by_field_name == "mlcategory", compare with ad_get_categories + paired ad_search_log_category_examples (baseline
vs. anomaly window).
Finish with a written RCA: root cause entity · affected jobs · temporal progression · fault class
(resource/network/application) · severity · recommended actions. Worked example:
references/worked-example.md. Full ES|QL templates and parameters:
references/investigate-anomaly-esql-tools.md.
Rules
- Multi-job entities are prime suspects; single-job entities are usually victims. Use
min_job_count=2.
- Earliest anomaly timestamp wins — sort
ad_rca_correlation by timestamp; first-appearing entity = origin.
multi_bucket_impact ≥ 3 = sustained behavioral shift, weight higher than transient spikes.
- Never close an RCA without
ad_rca_source_evidence — raw source documents are ground truth.
- Use low
min_score (25 or lower) for influencer queries — high thresholds miss correlated entities.
Mode: Explain — Score / model behavior
When: "why is my score 30/90?", "score dropped overnight", "what is renormalization?", "why wasn't this detected?".
Score types
| Field |
Scope |
Meaning |
record_score |
Single record |
Normalized severity after renormalization. |
initial_record_score |
Single record |
Score at detection time. Gap vs record_score = renormalization drift. |
anomaly_score |
Bucket |
Aggregate severity across all detectors in a bucket. |
influencer_score |
Entity × bucket |
How anomalous a specific entity is in that bucket. |
anomaly_score_explanation components
| Component |
Effect |
What it means |
anomaly_length |
↑ score |
More consecutive anomalous buckets |
single_bucket_impact |
↑ score |
Lower probability → higher impact |
multi_bucket_impact |
↑ score |
Sustained pattern contribution |
anomaly_characteristics_impact |
↑ score |
Mean shift vs. variance change |
high_variance_penalty |
↓ score |
Noisy data → wide bounds → anomaly less surprising |
incomplete_bucket_penalty |
↓ score |
Bucket has less data than expected (ingest lag, sparse data) |
Why a score looks wrong
- Unexpectedly low:
high_variance_penalty, renormalization, <3 weeks training for weekly seasonality,
bucket_span too large, wrong detector function (mean vs high_mean), incomplete_bucket_penalty, suppression by
custom_rules.
- Unexpectedly high: insufficient history (early training over-flags), high-cardinality split (too few points per
entity),
use_null: true on a sparse field.
Tool chain
| Purpose |
Tools |
| Records + explanation |
ad_query_anomaly_records (exact job_id_pattern) |
| Renormalization drift |
ad_rca_score_reassessment (score_drift = initial_record_score - record_score) |
| Model bounds (visual) |
ad_get_model_plot — actual outside model_lower/model_upper = anomaly |
| Forecast overlap |
ad_get_forecast_results |
| Influencer attribution |
ad_query_influencers |
| Config & detector |
ad_get_job_datafeed_config — bucket_span, function, custom_rules, use_null |
| Categorization |
ad_get_categories |
| Model snapshots |
ad_get_model_snapshots |
| Structured diagnostic |
ad_wf_troubleshoot_anomaly_score (full decision tree) |
Decision tree (ad_wf_troubleshoot_anomaly_score)
ad_get_jobs — ≥3 weeks data for weekly seasonality?
ad_ts_model_memory_health — memory_status healthy?
ad_ts_delayed_data_annotations — no incomplete buckets?
ad_query_anomaly_records — compare record_score vs initial_record_score.
ad_get_job_datafeed_config — bucket_span, detector function, custom_rules, use_null.
ad_get_model_plot — wide bounds → high_variance_penalty.
ad_rca_score_reassessment — renormalization drift across history.
- Explain
anomaly_score_explanation factors.
Rules
- Always show both
initial_record_score and record_score — the gap is the renormalization story.
- Explain renormalization before diagnosing config — score drift is the most common "score dropped" cause and needs
no config change.
actual << typical with count/low_count is an absence anomaly — distinguish outages from value spikes.
high_variance_penalty and incomplete_bucket_penalty explain most "low score" surprises without remediation.
- Weekly seasonality needs ≥3 weeks of training data — flag young jobs as the cause.
For detector function selection details, see
references/anomaly-detection-functions.md.
Mode: Troubleshoot — Job ops
When: "missing documents", "datafeed stopped", "hard_limit", "results look wrong", lifecycle changes, calendars,
CCS.
Common issues → fast paths
| Issue |
Fast path |
Full decision tree |
Missing docs / query_delay warning |
ad_ts_delayed_data_annotations → ad_ts_bucket_event_gaps → ad_ts_ingest_latency_estimate → ad_update_datafeed_query_delay |
ad_wf_troubleshoot_query_delay |
Memory soft_limit / hard_limit |
ad_ts_model_memory_health → ad_wf_ts_field_cardinality → ad_estimate_memory_requirement → ad_update_model_memory_limit |
ad_wf_troubleshoot_memory_limit |
| Datafeed not running / job state |
ad_get_jobs (state) → ad_get_job_messages → ad_manage_datafeed |
— |
CCS / remote_cluster: indices |
ad_ts_ccs_diagnostics |
— |
| Score sanity check |
— |
ad_wf_troubleshoot_anomaly_score |
hard_limit corrupts model state and causes downstream missing-doc false alarms (categorizer silently skips events
for unknown categories). Fix memory before fixing query_delay.
Memory concepts
| Field |
Meaning |
model_bytes |
Current memory used |
peak_model_bytes |
High-water mark since job opened |
model_bytes_memory_limit |
Configured model_memory_limit |
memory_status |
ok / soft_limit (pruning) / hard_limit (critical) |
total_by_field_count > 100k |
by_field cardinality too high — dominant driver |
total_partition_field_count > 10k |
Partition explosion |
total_category_count > 10k |
Too many distinct log patterns |
Prefer ad_estimate_memory_requirement (samples cardinality from source, calls Estimate Model Memory API) over
heuristics like peak_model_bytes * 1.3 — the heuristic ignores pure influencer and categorization memory.
Datafeed & timing concepts
query_delay — how far behind real time the datafeed queries. Too small → missing docs; too large → slower
alerts. Set to P95 ingest latency + buffer (default 60s–120s).
delayed_data_check_config — how aggressively the datafeed checks for late data.
bucket_span — analysis interval. Align with data granularity and detection window.
frequency — defaults to min(query_delay, bucket_span / 2).
Lifecycle for config changes (memory limit, query_delay)
- Stop datafeed:
ad_manage_datafeed (action=_stop)
- Close job
- Update config:
ad_update_model_memory_limit, ad_update_datafeed_query_delay,
ad_update_delayed_data_check_config
- Open job:
ad_open_job
- Start datafeed:
ad_manage_datafeed (action=_start)
Recover a corrupted period without resetting the whole model: ad_revert_model_snapshot.
Tool surface
| Category |
Tools |
| Permissions / metadata |
ad_validate_ml_tool_permissions, ad_get_available_metadata, ad_get_jobs |
| Job + datafeed state |
ad_get_job_datafeed_config, ad_get_job_messages, ad_manage_datafeed, ad_preview_datafeed_with_latency |
| Timing / missing docs |
ad_ts_delayed_data_annotations, ad_ts_bucket_event_gaps, ad_ts_ingest_latency_estimate, ad_update_datafeed_query_delay, ad_update_delayed_data_check_config, ad_wf_troubleshoot_query_delay |
| Memory |
ad_ts_model_memory_health, ad_wf_ts_field_cardinality, ad_estimate_memory_requirement, ad_update_model_memory_limit, ad_wf_troubleshoot_memory_limit |
| Model / lifecycle |
ad_get_model_snapshots, ad_revert_model_snapshot, ad_open_job, ad_create_job |
| CCS |
ad_ts_ccs_diagnostics |
| Calendars |
ad_get_calendar_events, ad_create_calendar_event |
Full parameter tables, ES|QL templates, and REST step lists:
references/troubleshoot-anomaly-tool-reference.md.
Rules
ad_validate_ml_tool_permissions first — missing privileges produce misleading empty results.
- Fix memory before
query_delay — hard_limit corrupts state; query_delay fixes on a memory-limited job are
wasted.
- Stop the datafeed before updating it. Updating a running datafeed is rejected.
- Close the job before updating memory limit. Sequence above.
- Prefer workflow tools (
ad_wf_*) over manually chaining diagnostics for complex decisions.
ad_preview_datafeed_with_latency before starting — confirm the datafeed returns data after config changes.
Mode: Manage — Create / configure jobs
When: "set up a job", "create an ML detector", "monitor X over time", "detect rare/unusual/anomalous values".
4-step workflow
PUT _ml/anomaly_detectors/<job_id> # 1. Define job (ad_create_job)
PUT _ml/datafeeds/datafeed-<job_id> # 2. Define datafeed (ad_create_datafeed)
POST _ml/anomaly_detectors/<job_id>/_open # 3a. Open job (ad_open_job)
POST _ml/datafeeds/datafeed-<job_id>/_start # 3b. Start datafeed (ad_manage_datafeed action=_start)
GET _ml/anomaly_detectors/<job_id>/results/records # 4. Read results
Process
Build configs. Parse the user request into job + datafeed JSON with no null fields.
Apply smart defaults:
| Field |
Default |
Override when |
bucket_span |
"15m" |
User specifies a different span |
time_field |
"@timestamp" |
User names a different timestamp field |
index |
"logs-*" |
User specifies an index or pattern |
datafeed_query |
{"match_all": {}} |
User mentions filters, processes, or time windows |
influencers |
by/over/partition fields from detectors |
User adds extra influencer fields |
job_id |
Generated from user description |
User provides an explicit ID |
query_delay |
"60s" |
P95 ingest latency is higher |
Choose detector function from user intent — full table in
references/anomaly-detection-functions.md:
- "high CPU" / "unusually large" →
high_mean or high_sum
- "rare logins" / "unusual values" →
rare (variants below)
- "too many requests" / "spike in count" →
high_count
rare variants:
- Infrequent globally →
rare by_field_name: X
- Infrequent vs peers →
rare by_field_name: X over_field_name: Y
- Infrequent per segment →
rare by_field_name: X partition_field_name: Y
- Infrequent per segment vs peers →
rare by_field_name: X over_field_name: Y partition_field_name: Z
Validate. platform.core.get_index_mapping on the target index to verify field existence/types →
ad_validate_job_spec. If errors, fix and re-validate (max 3 attempts).
Present and confirm. Show the complete job + datafeed bodies formatted as the exact API calls. Ask for
approval once. If feedback, incorporate and re-present (up to 3 rounds).
Deploy. After confirmation: ad_create_job → ad_create_datafeed → ad_open_job → ad_manage_datafeed
(action=_start). Report final job_id and datafeed_id.
For batch analysis on historical data, pass start and end to the datafeed start call.
Worked examples (rare-username, DNS exfil, large-downloads) with full JSON bodies and datafeed filters:
references/job-creation-recipes.md.
Rules
- Create job before datafeed. Datafeed references job by ID.
- Open job before starting datafeed. Start on a closed job is rejected.
query_delay = P95 ingest latency + buffer (60s–120s safe default).
- Forecasts require non-population jobs —
over_field_name jobs cannot be forecasted; warn before attempting.
by_field_name vs over_field_name: by compares entity to its own history; over compares to peer group in
the same bucket. partition_field_name = fully independent sub-model with its own normalization.
bucket_span matches detection granularity — 15m for high-frequency, 1h for operational metrics, 1d for daily
patterns. Larger smooths short spikes; smaller increases noise.
Registration (Kibana Agent Builder)
Requires Node.js 18+. Defaults to elastic/changeme when no credentials are supplied.
cd skills/kibana/kibana-anomaly-detection
# tools → workflows → skills
node scripts/kibana-agent-builder.mjs all register --kibana-url http://localhost:5601
# HTTPS with self-signed cert
node scripts/kibana-agent-builder.mjs all register --kibana-url https://localhost:5601 --insecure
all register runs tools register, then workflows register, then skills register. Kibana allows at most five
tool_ids per skill; the script fills them by scanning SKILL.md for tool mentions (in document order), then appends
ids from references/kibana/tools/esql/*.json until the cap (workflow-only tools omitted by default). If you run
skills register alone, run tools register first so those ids exist.
Workflow tool exclusions and prefixes live in scripts/agent_builder_constants.json.
MCP API key permissions:
- Kibana:
read_onechat, space_read
- Index:
read, view_index_metadata on .ml-anomalies-*, .ml-annotations-*, .ml-notifications-*, .ml-config
- For source evidence:
read on source data indices
Tool inventory
ES|QL tool specs live under references/kibana/tools/esql/*.json; workflow definitions under
references/kibana/workflows/*.yaml. Each Mode section above lists the tools it uses. Full surface:
references/tools.md (ES|QL) and references/workflow-tools.md
(workflows).
Key system indices
| Index |
Relevant content |
.ml-anomalies-* |
record, bucket, influencer, model_plot, model_forecast, model_snapshot, category_definition, model_size_stats |
.ml-config |
job/datafeed documents (visible even for never-run jobs) |
.ml-annotations-* |
delayed data (event == "delayed_data") |
.ml-notifications-* |
job messages (level: info/warning/error) |
Examples
RCA: "Something caused a spike in our error rate at 2pm — what broke?" → Investigate → ad_get_available_metadata →
ad_query_anomaly_timeline → ad_rca_cross_job_entity_match → ad_rca_multi_job_entities → RCA report.
Score drop: "My anomaly score went from 90 to 55 — did the model change?" → Explain → ad_rca_score_reassessment
for drift → explain renormalization if score_drift is large.
Memory limit: "Job status shows hard_limit and results look wrong." → Troubleshoot → ad_ts_model_memory_health →
ad_wf_ts_field_cardinality → ad_estimate_memory_requirement → ad_update_model_memory_limit (lifecycle: stop
datafeed → close → update → open → start).
New job: "Detect unusual error rates per host on nginx access logs." → Manage → high_count detector with
by_field_name: "host.keyword" → validate → present → deploy.
Multi-mode: "We had an incident last night, scores were high but now low — is the job healthy?" → Investigate the
incident → Explain the score drift → Troubleshoot if hard_limit or delayed data is suspected.
Guidelines
- Pick a mode first. Don't blend RCA logic with score-explanation logic in one response.
ad_validate_ml_tool_permissions first on empty results — privileges are the most common false-negative cause.
- Score bands are absolute thresholds:
>75 critical, 50–75 warning, 25–50 minor, <25 informational.
- Multi-job entities are prime suspects. Use
min_job_count=2 in ad_rca_multi_job_entities.
- Show
initial_record_score alongside record_score — the gap tells the renormalization story.
- Fix memory before
query_delay. hard_limit invalidates downstream diagnostics.
- Stop datafeed → close job → update config → open job → start datafeed for any config change to memory or query
delay.
- Confirm RCAs with
ad_rca_source_evidence. Raw source documents are ground truth.
1---2name: kibana-anomaly-detection3description: Elastic ML anomaly detection skill — investigation/RCA, score explanation, job operations (create, datafeed, start/stop, results), and troubleshooting (missing docs, memory limits, datafeed health, lifecycle). Operates against Kibana Agent Builder MCP tools (`ad_*`) on `.ml-anomalies-*`, `.ml-config`, `.ml-notifications-*`, `.ml-annotations-*`. Use when answering "what broke?"/"which entity?"/RCA, "why is score high/low?"/renormalization, "datafeed stopped"/"memory limit", or any request to set up or configure an ML anomaly detection job.4---56# Elastic ML Anomaly Detection78Single skill covering all anomaly detection work against **Kibana Agent Builder** MCP at9`{KIBANA_URL}/api/agent_builder/mcp`. Use the **Mode Selector** below to pick the right approach for the user's question10— modes share the same tool surface and concepts.1112## Platform1314- Read path: ES|QL against `.ml-anomalies-*`, `.ml-config`, `.ml-notifications-*`, `.ml-annotations-*`15- Always-available: `platform.core.execute_esql` (plus additional platform tools for search, index mapping, and16 documentation — see `scripts/agent_builder_constants.json`)17- ML API spec (if available): `.kibana_ai_openapi_spec_elasticsearch` — see18 [references/anomaly-detection-openapi-spec-discover.md](references/anomaly-detection-openapi-spec-discover.md) for19 discovery pattern.20- **Run `ad_validate_ml_tool_permissions` first** when tools return empty/misleading results — missing privileges are21 the most common cause of false negatives. Full permissions matrix:22 [references/permissions-matrix.md](references/permissions-matrix.md).2324## Mode Selector2526| User intent | Mode |27| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |28| "What broke?" / RCA / cross-job / blast radius / influencers / log categories | **Investigate** |29| "Why score high/low?" / renormalization / model bounds / forecasts | **Explain** |30| Missing docs / memory limit / datafeed stopped / CCS / lifecycle / calendars | **Troubleshoot** |31| Create a job / configure a datafeed / start analysis / retrieve results | **Manage** |32| Security framing (attack chains, MITRE, exfil) | Investigate + [references/security-anomaly-expert.md](references/security-anomaly-expert.md) |33| Observability/SRE framing (degradation, capacity, deployment regression) | Investigate + [references/observability-anomaly-expert.md](references/observability-anomaly-expert.md) |3435When a question spans modes: **Investigate → Explain → Troubleshoot**. Don't blend mode logic — finish one before moving36on.3738---3940## Score Quick Reference4142- `record_score` bands: **>75** critical · **50–75** warning · **25–50** minor · **<25** informational43- `multi_bucket_impact ≥ 3` → sustained shift (not a transient spike)44- `initial_record_score >> record_score` → renormalization (model saw worse anomalies later)45- `actual << typical` with `count`/`low_count`/`low_mean` → absence/outage, not just low value46- Low scores across many jobs > one high score — composite cross-job signal often beats single-detector severity4748> Full score definitions, renormalization mechanics, and `anomaly_score_explanation` components:49> [references/score-reference.md](references/score-reference.md).5051## Core concepts5253Treat `.ml-anomalies-*` as three layers, accessed via `result_type`:5455- **`bucket`** — bucket-level unusualness per `bucket_span`. `anomaly_score` is the aggregate across all detectors.56- **`record`** — finest-grained rows with `actual` vs `typical`, `probability`, `record_score`,57 `anomaly_score_explanation`.58- **`influencer`** — entity contributions ranked within a bucket (`influencer_score`).5960Read scores this way:6162- `anomaly_score` / `record_score` = **current normalized** values (move as the model sees new extremes).63- `initial_anomaly_score` / `initial_record_score` = **immutable snapshots** from detection time.64- Compare `actual` to `typical`; use `probability` for raw likelihood.65- Map entities via `partition_field_value` / `by_field_value` / `over_field_value`.66- Read `multi_bucket_impact` (-5 to +5) to separate single-bucket spikes from sustained trends.6768---6970## Mode: Investigate — RCA7172**When:** "what broke?", "which entity caused this?", cross-job correlation, blast radius, attack/cascade chains.7374### Tool chain7576| Phase | Tools |77| --------------------- | -------------------------------------------------------------------------------------------------------------- |78| Discovery | `ad_get_available_metadata`, `ad_get_jobs`, `ad_discover_related_jobs`, `ad_discover_jobs_by_datafeed_index` |79| Timeline / scope | `ad_query_anomaly_timeline` |80| Cross-job / entities | `ad_rca_cross_job_entity_match`, `ad_rca_multi_job_entities`, `ad_rca_entity_profile` |81| Records / influencers | `ad_query_anomaly_records`, `ad_query_influencers` |82| RCA depth | `ad_rca_detector_fingerprint`, `ad_rca_correlation`, `ad_rca_blast_radius`, `ad_rca_score_reassessment` |83| Evidence / categories | `ad_get_job_datafeed_config`, `ad_rca_source_evidence`, `ad_get_categories`, `ad_search_log_category_examples` |8485### Protocol8687Follow the 14-step sequence in [references/protocols/investigation.md](references/protocols/investigation.md). High88level: `ad_get_available_metadata` → pair `ad_discover_jobs_by_datafeed_index` with `ad_discover_related_jobs` →89`ad_query_anomaly_timeline` → rank with `ad_rca_multi_job_entities` (`min_job_count=2`) → `ad_rca_detector_fingerprint`90→ drill with `ad_query_anomaly_records` + `ad_query_influencers` (low `min_score=25`) → profile with91`ad_rca_entity_profile` → order with `ad_rca_correlation` → confirm with `ad_rca_source_evidence`. When92`by_field_name == "mlcategory"`, compare with `ad_get_categories` + paired `ad_search_log_category_examples` (baseline93vs. anomaly window).9495Finish with a written RCA: **root cause entity · affected jobs · temporal progression · fault class96(resource/network/application) · severity · recommended actions**. Worked example:97[references/worked-example.md](references/worked-example.md). Full ES|QL templates and parameters:98[references/investigate-anomaly-esql-tools.md](references/investigate-anomaly-esql-tools.md).99100### Rules1011021. **Multi-job entities are prime suspects; single-job entities are usually victims.** Use `min_job_count=2`.1032. **Earliest anomaly timestamp wins** — sort `ad_rca_correlation` by timestamp; first-appearing entity = origin.1043. **`multi_bucket_impact ≥ 3` = sustained behavioral shift**, weight higher than transient spikes.1054. **Never close an RCA without `ad_rca_source_evidence`** — raw source documents are ground truth.1065. **Use low `min_score` (25 or lower) for influencer queries** — high thresholds miss correlated entities.107108---109110## Mode: Explain — Score / model behavior111112**When:** "why is my score 30/90?", "score dropped overnight", "what is renormalization?", "why wasn't this detected?".113114### Score types115116| Field | Scope | Meaning |117| ---------------------- | --------------- | ----------------------------------------------------------------------- |118| `record_score` | Single record | Normalized severity after renormalization. |119| `initial_record_score` | Single record | Score at detection time. Gap vs `record_score` = renormalization drift. |120| `anomaly_score` | Bucket | Aggregate severity across all detectors in a bucket. |121| `influencer_score` | Entity × bucket | How anomalous a specific entity is in that bucket. |122123### `anomaly_score_explanation` components124125| Component | Effect | What it means |126| -------------------------------- | ------- | ------------------------------------------------------------ |127| `anomaly_length` | ↑ score | More consecutive anomalous buckets |128| `single_bucket_impact` | ↑ score | Lower probability → higher impact |129| `multi_bucket_impact` | ↑ score | Sustained pattern contribution |130| `anomaly_characteristics_impact` | ↑ score | Mean shift vs. variance change |131| `high_variance_penalty` | ↓ score | Noisy data → wide bounds → anomaly less surprising |132| `incomplete_bucket_penalty` | ↓ score | Bucket has less data than expected (ingest lag, sparse data) |133134### Why a score looks wrong135136- **Unexpectedly low:** `high_variance_penalty`, renormalization, <3 weeks training for weekly seasonality,137 `bucket_span` too large, wrong detector function (`mean` vs `high_mean`), `incomplete_bucket_penalty`, suppression by138 `custom_rules`.139- **Unexpectedly high:** insufficient history (early training over-flags), high-cardinality split (too few points per140 entity), `use_null: true` on a sparse field.141142### Tool chain143144| Purpose | Tools |145| ---------------------- | ---------------------------------------------------------------------------------- |146| Records + explanation | `ad_query_anomaly_records` (exact `job_id_pattern`) |147| Renormalization drift | `ad_rca_score_reassessment` (`score_drift = initial_record_score - record_score`) |148| Model bounds (visual) | `ad_get_model_plot` — actual outside `model_lower`/`model_upper` = anomaly |149| Forecast overlap | `ad_get_forecast_results` |150| Influencer attribution | `ad_query_influencers` |151| Config & detector | `ad_get_job_datafeed_config` — `bucket_span`, function, `custom_rules`, `use_null` |152| Categorization | `ad_get_categories` |153| Model snapshots | `ad_get_model_snapshots` |154| Structured diagnostic | **`ad_wf_troubleshoot_anomaly_score`** (full decision tree) |155156### Decision tree (`ad_wf_troubleshoot_anomaly_score`)1571581. `ad_get_jobs` — ≥3 weeks data for weekly seasonality?1592. `ad_ts_model_memory_health` — `memory_status` healthy?1603. `ad_ts_delayed_data_annotations` — no incomplete buckets?1614. `ad_query_anomaly_records` — compare `record_score` vs `initial_record_score`.1625. `ad_get_job_datafeed_config` — `bucket_span`, detector function, `custom_rules`, `use_null`.1636. `ad_get_model_plot` — wide bounds → `high_variance_penalty`.1647. `ad_rca_score_reassessment` — renormalization drift across history.1658. Explain `anomaly_score_explanation` factors.166167### Rules1681691. **Always show both `initial_record_score` and `record_score`** — the gap is the renormalization story.1702. **Explain renormalization before diagnosing config** — score drift is the most common "score dropped" cause and needs171 no config change.1723. **`actual << typical` with `count`/`low_count` is an absence anomaly** — distinguish outages from value spikes.1734. **`high_variance_penalty` and `incomplete_bucket_penalty` explain most "low score" surprises** without remediation.1745. **Weekly seasonality needs ≥3 weeks of training data** — flag young jobs as the cause.175176For detector function selection details, see177[references/anomaly-detection-functions.md](references/anomaly-detection-functions.md).178179---180181## Mode: Troubleshoot — Job ops182183**When:** "missing documents", "datafeed stopped", "hard_limit", "results look wrong", lifecycle changes, calendars,184CCS.185186### Common issues → fast paths187188| Issue | Fast path | Full decision tree |189| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |190| Missing docs / `query_delay` warning | `ad_ts_delayed_data_annotations` → `ad_ts_bucket_event_gaps` → `ad_ts_ingest_latency_estimate` → `ad_update_datafeed_query_delay` | `ad_wf_troubleshoot_query_delay` |191| Memory `soft_limit` / `hard_limit` | `ad_ts_model_memory_health` → `ad_wf_ts_field_cardinality` → `ad_estimate_memory_requirement` → `ad_update_model_memory_limit` | `ad_wf_troubleshoot_memory_limit` |192| Datafeed not running / job state | `ad_get_jobs` (state) → `ad_get_job_messages` → `ad_manage_datafeed` | — |193| CCS / `remote_cluster:` indices | `ad_ts_ccs_diagnostics` | — |194| Score sanity check | — | `ad_wf_troubleshoot_anomaly_score` |195196> `hard_limit` corrupts model state and causes downstream missing-doc false alarms (categorizer silently skips events197> for unknown categories). **Fix memory before fixing `query_delay`.**198199### Memory concepts200201| Field | Meaning |202| ----------------------------------- | ------------------------------------------------------- |203| `model_bytes` | Current memory used |204| `peak_model_bytes` | High-water mark since job opened |205| `model_bytes_memory_limit` | Configured `model_memory_limit` |206| `memory_status` | `ok` / `soft_limit` (pruning) / `hard_limit` (critical) |207| `total_by_field_count > 100k` | `by_field` cardinality too high — dominant driver |208| `total_partition_field_count > 10k` | Partition explosion |209| `total_category_count > 10k` | Too many distinct log patterns |210211Prefer **`ad_estimate_memory_requirement`** (samples cardinality from source, calls Estimate Model Memory API) over212heuristics like `peak_model_bytes * 1.3` — the heuristic ignores pure influencer and categorization memory.213214### Datafeed & timing concepts215216- **`query_delay`** — how far behind real time the datafeed queries. Too small → missing docs; too large → slower217 alerts. Set to **P95 ingest latency + buffer** (default `60s`–`120s`).218- **`delayed_data_check_config`** — how aggressively the datafeed checks for late data.219- **`bucket_span`** — analysis interval. Align with data granularity and detection window.220- **`frequency`** — defaults to `min(query_delay, bucket_span / 2)`.221222### Lifecycle for config changes (memory limit, query_delay)2232241. Stop datafeed: `ad_manage_datafeed` (`action=_stop`)2252. Close job2263. Update config: `ad_update_model_memory_limit`, `ad_update_datafeed_query_delay`,227 `ad_update_delayed_data_check_config`2284. Open job: `ad_open_job`2295. Start datafeed: `ad_manage_datafeed` (`action=_start`)230231Recover a corrupted period without resetting the whole model: `ad_revert_model_snapshot`.232233### Tool surface234235| Category | Tools |236| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |237| Permissions / metadata | `ad_validate_ml_tool_permissions`, `ad_get_available_metadata`, `ad_get_jobs` |238| Job + datafeed state | `ad_get_job_datafeed_config`, `ad_get_job_messages`, `ad_manage_datafeed`, `ad_preview_datafeed_with_latency` |239| Timing / missing docs | `ad_ts_delayed_data_annotations`, `ad_ts_bucket_event_gaps`, `ad_ts_ingest_latency_estimate`, `ad_update_datafeed_query_delay`, `ad_update_delayed_data_check_config`, `ad_wf_troubleshoot_query_delay` |240| Memory | `ad_ts_model_memory_health`, `ad_wf_ts_field_cardinality`, `ad_estimate_memory_requirement`, `ad_update_model_memory_limit`, `ad_wf_troubleshoot_memory_limit` |241| Model / lifecycle | `ad_get_model_snapshots`, `ad_revert_model_snapshot`, `ad_open_job`, `ad_create_job` |242| CCS | `ad_ts_ccs_diagnostics` |243| Calendars | `ad_get_calendar_events`, `ad_create_calendar_event` |244245Full parameter tables, ES|QL templates, and REST step lists:246[references/troubleshoot-anomaly-tool-reference.md](references/troubleshoot-anomaly-tool-reference.md).247248### Rules2492501. **`ad_validate_ml_tool_permissions` first** — missing privileges produce misleading empty results.2512. **Fix memory before `query_delay`** — `hard_limit` corrupts state; `query_delay` fixes on a memory-limited job are252 wasted.2533. **Stop the datafeed before updating it.** Updating a running datafeed is rejected.2544. **Close the job before updating memory limit.** Sequence above.2555. **Prefer workflow tools (`ad_wf_*`) over manually chaining diagnostics** for complex decisions.2566. **`ad_preview_datafeed_with_latency` before starting** — confirm the datafeed returns data after config changes.257258---259260## Mode: Manage — Create / configure jobs261262**When:** "set up a job", "create an ML detector", "monitor X over time", "detect rare/unusual/anomalous values".263264### 4-step workflow265266```text267PUT _ml/anomaly_detectors/<job_id> # 1. Define job (ad_create_job)268PUT _ml/datafeeds/datafeed-<job_id> # 2. Define datafeed (ad_create_datafeed)269POST _ml/anomaly_detectors/<job_id>/_open # 3a. Open job (ad_open_job)270POST _ml/datafeeds/datafeed-<job_id>/_start # 3b. Start datafeed (ad_manage_datafeed action=_start)271GET _ml/anomaly_detectors/<job_id>/results/records # 4. Read results272```273274### Process2752761. **Build configs.** Parse the user request into job + datafeed JSON with no null fields.2772. **Apply smart defaults:**278279 | Field | Default | Override when |280 | ---------------- | --------------------------------------- | ------------------------------------------------- |281 | `bucket_span` | `"15m"` | User specifies a different span |282 | `time_field` | `"@timestamp"` | User names a different timestamp field |283 | `index` | `"logs-*"` | User specifies an index or pattern |284 | `datafeed_query` | `{"match_all": {}}` | User mentions filters, processes, or time windows |285 | `influencers` | by/over/partition fields from detectors | User adds extra influencer fields |286 | `job_id` | Generated from user description | User provides an explicit ID |287 | `query_delay` | `"60s"` | P95 ingest latency is higher |2882893. **Choose detector function** from user intent — full table in290 [references/anomaly-detection-functions.md](references/anomaly-detection-functions.md):291 - "high CPU" / "unusually large" → `high_mean` or `high_sum`292 - "rare logins" / "unusual values" → `rare` (variants below)293 - "too many requests" / "spike in count" → `high_count`294295 `rare` variants:296 - Infrequent globally → `rare by_field_name: X`297 - Infrequent vs peers → `rare by_field_name: X over_field_name: Y`298 - Infrequent per segment → `rare by_field_name: X partition_field_name: Y`299 - Infrequent per segment vs peers → `rare by_field_name: X over_field_name: Y partition_field_name: Z`3003014. **Validate.** `platform.core.get_index_mapping` on the target index to verify field existence/types →302 `ad_validate_job_spec`. If errors, fix and re-validate (max 3 attempts).3033045. **Present and confirm.** Show the **complete** job + datafeed bodies formatted as the exact API calls. Ask for305 approval **once**. If feedback, incorporate and re-present (up to 3 rounds).3063076. **Deploy.** After confirmation: `ad_create_job` → `ad_create_datafeed` → `ad_open_job` → `ad_manage_datafeed`308 (`action=_start`). Report final `job_id` and `datafeed_id`.309310For **batch analysis on historical data**, pass `start` and `end` to the datafeed start call.311312> Worked examples (rare-username, DNS exfil, large-downloads) with full JSON bodies and datafeed filters:313> [references/job-creation-recipes.md](references/job-creation-recipes.md).314315### Rules3163171. **Create job before datafeed.** Datafeed references job by ID.3182. **Open job before starting datafeed.** Start on a closed job is rejected.3193. **`query_delay` = P95 ingest latency + buffer** (60s–120s safe default).3204. **Forecasts require non-population jobs** — `over_field_name` jobs cannot be forecasted; warn before attempting.3215. **`by_field_name` vs `over_field_name`:** `by` compares entity to its own history; `over` compares to peer group in322 the same bucket. `partition_field_name` = fully independent sub-model with its own normalization.3236. **`bucket_span` matches detection granularity** — 15m for high-frequency, 1h for operational metrics, 1d for daily324 patterns. Larger smooths short spikes; smaller increases noise.325326---327328## Registration (Kibana Agent Builder)329330Requires Node.js 18+. Defaults to `elastic`/`changeme` when no credentials are supplied.331332```bash333cd skills/kibana/kibana-anomaly-detection334335# tools → workflows → skills336node scripts/kibana-agent-builder.mjs all register --kibana-url http://localhost:5601337338# HTTPS with self-signed cert339node scripts/kibana-agent-builder.mjs all register --kibana-url https://localhost:5601 --insecure340```341342`all register` runs `tools register`, then `workflows register`, then `skills register`. Kibana allows **at most five**343`tool_ids` per skill; the script fills them by scanning `SKILL.md` for tool mentions (in document order), then appends344ids from `references/kibana/tools/esql/*.json` until the cap (workflow-only tools omitted by default). If you run345`skills register` alone, run `tools register` first so those ids exist.346347Workflow tool exclusions and prefixes live in `scripts/agent_builder_constants.json`.348349**MCP API key permissions:**350351- Kibana: `read_onechat`, `space_read`352- Index: `read`, `view_index_metadata` on `.ml-anomalies-*`, `.ml-annotations-*`, `.ml-notifications-*`, `.ml-config`353- For source evidence: `read` on source data indices354355---356357## Tool inventory358359ES|QL tool specs live under `references/kibana/tools/esql/*.json`; workflow definitions under360`references/kibana/workflows/*.yaml`. Each Mode section above lists the tools it uses. Full surface:361[references/tools.md](references/tools.md) (ES|QL) and [references/workflow-tools.md](references/workflow-tools.md)362(workflows).363364### Key system indices365366| Index | Relevant content |367| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |368| `.ml-anomalies-*` | `record`, `bucket`, `influencer`, `model_plot`, `model_forecast`, `model_snapshot`, `category_definition`, `model_size_stats` |369| `.ml-config` | job/datafeed documents (visible even for never-run jobs) |370| `.ml-annotations-*` | delayed data (`event == "delayed_data"`) |371| `.ml-notifications-*` | job messages (`level`: info/warning/error) |372373---374375## Examples376377**RCA:** "Something caused a spike in our error rate at 2pm — what broke?" → Investigate → `ad_get_available_metadata` →378`ad_query_anomaly_timeline` → `ad_rca_cross_job_entity_match` → `ad_rca_multi_job_entities` → RCA report.379380**Score drop:** "My anomaly score went from 90 to 55 — did the model change?" → Explain → `ad_rca_score_reassessment`381for drift → explain renormalization if `score_drift` is large.382383**Memory limit:** "Job status shows `hard_limit` and results look wrong." → Troubleshoot → `ad_ts_model_memory_health` →384`ad_wf_ts_field_cardinality` → `ad_estimate_memory_requirement` → `ad_update_model_memory_limit` (lifecycle: stop385datafeed → close → update → open → start).386387**New job:** "Detect unusual error rates per host on nginx access logs." → Manage → `high_count` detector with388`by_field_name: "host.keyword"` → validate → present → deploy.389390**Multi-mode:** "We had an incident last night, scores were high but now low — is the job healthy?" → Investigate the391incident → Explain the score drift → Troubleshoot if `hard_limit` or delayed data is suspected.392393---394395## Guidelines3963971. **Pick a mode first.** Don't blend RCA logic with score-explanation logic in one response.3982. **`ad_validate_ml_tool_permissions` first** on empty results — privileges are the most common false-negative cause.3993. **Score bands are absolute thresholds**: `>75` critical, `50–75` warning, `25–50` minor, `<25` informational.4004. **Multi-job entities are prime suspects.** Use `min_job_count=2` in `ad_rca_multi_job_entities`.4015. **Show `initial_record_score` alongside `record_score`** — the gap tells the renormalization story.4026. **Fix memory before `query_delay`.** `hard_limit` invalidates downstream diagnostics.4037. **Stop datafeed → close job → update config → open job → start datafeed** for any config change to memory or query404 delay.4058. **Confirm RCAs with `ad_rca_source_evidence`.** Raw source documents are ground truth.