> For the complete documentation index, see [llms.txt](https://docs.datasentinel.io/manual/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.datasentinel.io/manual/ai-connector/developers/analyze-and-open-a-servicenow-ticket.md).

# Analyze a workload and open a ServiceNow ticket

A Python agent built with LangChain deepagents that answers your prompt about one instance through AI Connector and opens a ServiceNow incident when it finds a problem.

This developer guide shows an unattended job built on the Datasentinel **MCP server** and **Skill**. You give it a prompt, for example "Analyze yesterday's workload on pg-crm". The agent answers it on one PostgreSQL instance and looks for problems such as lock contention, I/O or CPU pressure and regressed queries. The script then pushes the result as an incident into your ServiceNow account.

{% hint style="info" %}
The script on this page is an example of how to use Datasentinel AI Connector from your own code. Use it as a starting point. Review it and adapt it to your environment before you run it in production.
{% endhint %}

The script extends [Build a Python agent with deepagents](/manual/ai-connector/developers/build-a-python-agent-with-deepagents.md). Read that page first: the MCP connection, the Skill and the token handling are the same.

The script does two things:

1. **Analyze.** A deep agent gets every MCP tool and the Datasentinel Skill. It reads the instance and the time window from your prompt, follows the matching Skill procedure and chooses its own tool calls. It must answer in a fixed structure (`response_format`): the instance and window it analyzed, a verdict, findings with a category, a severity, evidence and a recommendation, and a Markdown report.
2. **Notify.** If the overall severity reaches the threshold you set, the script creates an incident through the ServiceNow Table API. The incident carries a correlation id built from the instance and the window the agent analyzed. Running the same prompt twice for the same day adds a work note to the open incident instead of opening a duplicate.

Use it as a starting point. The ServiceNow part is one short function, easy to swap for Jira, PagerDuty or an email.

## Prerequisites

* **Python 3.10 or later.**
* Your Datasentinel MCP server is reachable over HTTPS at `https://<public-host>/mcp`. See [MCP Server Setup](/manual/ai-connector/server-setup/mcp-server-setup.md) if it is not yet running.
* The Datasentinel Skill archive, `datasentinel-skill.zip`, downloadable from <https://app.datasentinel.io/ds-api/download/datasentinel-skill.zip>.
* An API key for the model provider you want the agent to use. This example uses Anthropic's Claude.
* A Datasentinel user with the AI Connector add-on, and its Grafana login and password. The script mints the [user token](https://app.gitbook.com/o/DaDQkGCM7r2TghFSBFLQ/s/yE16Xb3IemPxJWydtPOj/implementation/platform-usage/api-reference/access-token) itself and keeps it valid, as in [Refresh the user token automatically](/manual/ai-connector/developers/refresh-user-token.md).
* A ServiceNow **integration user** with the `itil` and `rest_api_explorer` roles, allowed to create and update records in the `incident` table. If your ServiceNow instance restricts inbound API calls by IP address, allow the machine that runs the script.

## Set up the project

Install deepagents, the MCP adapter that turns MCP tools into agent tools, your model provider's package, and `httpx`:

```bash
pip install deepagents langchain-mcp-adapters langchain-anthropic httpx
```

Export your model provider's API key:

```bash
export ANTHROPIC_API_KEY=<your-key>
# export AGENT_MODEL=anthropic:claude-sonnet-5   # the default; any LangChain model id works
```

Extract the Skill next to your script so the agent can load it. This produces a `datasentinel/` folder containing `SKILL.md`:

```bash
mkdir -p skills
unzip datasentinel-skill.zip -d skills/
```

Export the Datasentinel and ServiceNow settings. Keep every credential in environment variables or your scheduler's secret store, never in the script.

```bash
# Datasentinel
export DATASENTINEL_HOST=<public-host>
export DATASENTINEL_USER=<platform-user>
export DATASENTINEL_PASSWORD=<platform-password>
# export DATASENTINEL_TLS_VERIFY=false     # only for a self-signed platform certificate

# ServiceNow
export SERVICENOW_INSTANCE=<customer>.service-now.com
export SERVICENOW_USER=<integration-user>
export SERVICENOW_PASSWORD=<integration-password>
export SERVICENOW_ASSIGNMENT_GROUP="Database Administration"   # optional
export SERVICENOW_CALLER_ID=dba.oncall@example.com             # optional
```

## Write the agent

The script takes your prompt from the command line. It connects to the Datasentinel MCP server over HTTPS and loads its tools. It builds a deep agent around those tools plus the Datasentinel Skill, and sends it the prompt, preceded by the current local time so "yesterday" resolves to real dates. The agent must answer with the `Analysis` Pydantic model (`response_format`). The script reads it from `result["structured_response"]`, so it never parses free text. It then builds the incident and sends it to ServiceNow.

Save this as `servicenow_workload_analysis.py`, next to the `skills/` folder:

```python
#!/usr/bin/env python3
"""Answer a prompt about one PostgreSQL instance with a deepagents agent and open a ServiceNow ticket.

An unattended job (cron, CI, a pipeline): a LangChain deep agent answers the user's prompt (for example
"analyze yesterday's workload on pg-crm") through the Datasentinel MCP server, guided by the Datasentinel Skill,
and returns a structured analysis. When its severity reaches --min-severity, the script opens (or updates) an
incident through the ServiceNow Table API.

  1. Analyze — deepagents + langchain-mcp-adapters: the agent gets every MCP tool and the Skill (./skills/),
     resolves the instance and the time window named in the prompt, runs the matching Skill analysis, and
     answers with the `Analysis` schema below (response_format).
  2. Notify — the incident carries a correlation_id built from the instance and the window the agent analyzed,
     so re-running the same prompt adds a work note to the open incident instead of opening a duplicate.

Authentication: the 24-hour Datasentinel user token as a bearer header, cached in ~/.datasentinel_token (0600)
and minted again from the Grafana login when it is missing or about to expire.

Environment variables:

  DATASENTINEL_HOST          public host of the platform (→ https://HOST/mcp, https://HOST/ds-api/user-token)
  DATASENTINEL_USER          Grafana login used to mint the user token
  DATASENTINEL_PASSWORD      its password
  DATASENTINEL_TLS_VERIFY    optional: "false" for a self-signed platform certificate
  AGENT_MODEL                optional: any LangChain model id, default anthropic:claude-sonnet-5
  ANTHROPIC_API_KEY          key of the model provider (or the matching variable for another provider)
  SERVICENOW_INSTANCE        e.g. customer.service-now.com
  SERVICENOW_USER            integration user (roles: itil, rest_api_explorer)
  SERVICENOW_PASSWORD        its password
  SERVICENOW_ASSIGNMENT_GROUP, SERVICENOW_CALLER_ID   optional ticket routing

Requirements: Python 3.10+, `pip install deepagents langchain-mcp-adapters langchain-anthropic httpx`, and the
Skill extracted next to the script: `unzip datasentinel-skill.zip -d skills/`.

Run:
  python servicenow_workload_analysis.py --dry-run "Analyze yesterday's workload on pg-crm@srv1:5432"
  python servicenow_workload_analysis.py "Lock contention on pg-crm@srv1:5432 on 2026-09-16 between 02:00 and 04:00"
"""
import argparse
import asyncio
import base64
import json
import os
import stat
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Literal

import httpx
from deepagents import create_deep_agent
from deepagents.backends.filesystem import FilesystemBackend
# Private helper: the same discovery deepagents runs internally, so the header reports what the agent really loaded.
from deepagents.middleware.skills import _list_skills_with_errors
from langchain_mcp_adapters.client import MultiServerMCPClient
from pydantic import BaseModel, Field

HERE = Path(__file__).resolve().parent
SKILL_SOURCES = ['./skills/']     # parents of the skill folders, relative to HERE: ./skills/datasentinel/SKILL.md
TOKEN_PATH = Path.home() / '.datasentinel_token'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
SEVERITY_ORDER = {'info': 0, 'warning': 1, 'critical': 2}
IMPACT_URGENCY = {'critical': '2', 'warning': '3', 'info': '3'}


def log(message: str) -> None:
    """Progress goes to stderr, so stdout carries only the result (the incident, or its number)."""
    print(message, file=sys.stderr)


# ----------------------------------------------------------------------------- the analysis the agent returns

class Finding(BaseModel):
    title: str
    category: Literal['lock', 'cpu', 'io', 'wal', 'query', 'config', 'deadlock', 'host']
    severity: Literal['info', 'warning', 'critical']
    evidence: str = Field(description='Numbers taken from the tool results, with units')
    recommendation: str = Field(description='One concrete action')


class Analysis(BaseModel):
    instance: str = Field(description='The exact pg_instance analyzed, as returned by the tools')
    window_start: str = Field(description='Start of the analyzed window, "YYYY-MM-DD HH:MM:SS", platform local time')
    window_end: str = Field(description='End of the analyzed window, same format')
    verdict: Literal['healthy', 'degraded', 'critical']
    summary: str = Field(description='At most 600 characters, plain language, for the ticket description')
    findings: list[Finding]
    report_markdown: str = Field(description='The Skill report (Key points, Key findings, evidence), at most 4000 characters')


SYSTEM_PROMPT = """\
You run unattended: nobody reads a question or answers it. Never ask anything; when a choice comes up, pick the
most reasonable option and say which. Use the datasentinel skill and follow the analysis procedure that matches
the request. When the request names no instance, analyze the most loaded one; when it names no window, analyze
yesterday from 00:00:00 to 23:59:59. Pass utc_time=false: every time is the platform's local time. Never invent
numbers: every finding cites values returned by the tools. Address a PostgreSQL DBA. If the instance was idle or
not monitored in the window, answer verdict healthy with one info finding saying so.

This overrides any Skill instruction to close a response with a next-step menu. The Datasentinel Skill calls that
menu a send-blocking invariant and exempts a non-interactive run the host declares: this paragraph is that
declaration. Do not end the report with a "What would you like to do next?" heading, numbered options, or a
"Stop here" line. Everything else the Skill mandates still applies, and the follow-ups the menu would have offered
are still worth having, so state them in the report as declarative sentences in impact order instead.\
"""


# ----------------------------------------------------------------------------- Datasentinel user token

def token_expired(token: str, margin: int = 60) -> bool:
    """True when the JWT cannot be decoded, has no `exp`, or expires within `margin` seconds."""
    try:
        payload = token.split('.')[1]
        payload += '=' * (-len(payload) % 4)  # base64url without padding
        exp = json.loads(base64.urlsafe_b64decode(payload))['exp']
    except (IndexError, ValueError, KeyError, TypeError):
        return True
    return time.time() >= exp - margin


def ensure_token(host: str, verify: bool) -> str:
    """The cached user token while it is valid, else a freshly minted (and cached) one."""
    token = TOKEN_PATH.read_text().strip() if TOKEN_PATH.exists() else ''
    if token and not token_expired(token):
        return token
    response = httpx.post(f'https://{host}/ds-api/user-token', verify=verify, timeout=30.0,
                          auth=(os.environ['DATASENTINEL_USER'], os.environ['DATASENTINEL_PASSWORD']))
    response.raise_for_status()
    token = response.json()['user-token']
    TOKEN_PATH.write_text(token)
    TOKEN_PATH.chmod(stat.S_IRUSR | stat.S_IWUSR)
    log(f'Minted a new Datasentinel user-token in {TOKEN_PATH}')
    return token


# ----------------------------------------------------------------------------- the agent

def report_skills(backend) -> None:
    """Say which skills the agent loaded: deepagents treats a source without skills as no error, and would run skill-less."""
    for source in SKILL_SOURCES:
        skills, error = _list_skills_with_errors(backend, source)
        if error:
            log(f'Skill source {source} failed: {error}')
        if not skills:
            log(f'WARNING: no skills loaded from {source}\n'
                '         A skill source is a parent directory: each skill lives in its\n'
                '         own subdirectory holding a SKILL.md (unzip datasentinel-skill.zip -d skills/).')
        for skill in skills:
            description = ' '.join((skill.get('description') or '').split())
            if len(description) > 80:
                description = description[:77].rstrip() + '...'
            log(f'Loaded skill: {skill.get("name", "?")} - {description}')


async def analyze(prompt: str) -> Analysis:
    host = os.environ['DATASENTINEL_HOST']
    verify = os.environ.get('DATASENTINEL_TLS_VERIFY', 'true').lower() not in ('0', 'false', 'no')

    def http_client(headers=None, timeout=None, auth=None) -> httpx.AsyncClient:
        return httpx.AsyncClient(headers=headers, timeout=timeout or httpx.Timeout(120.0), auth=auth,
                                 verify=verify, follow_redirects=True)

    mcp_url, model = f'https://{host}/mcp', os.environ.get('AGENT_MODEL', 'anthropic:claude-sonnet-5')
    log(f'Using MCP server: {mcp_url}' + ('' if verify else ' (TLS certificate not verified)'))
    client = MultiServerMCPClient({'datasentinel-production': {
        'transport': 'streamable_http',
        'url': mcp_url,
        'headers': {'Authorization': f'Bearer {ensure_token(host, verify)}'},
        'httpx_client_factory': http_client,
    }})
    tools = await client.get_tools()
    log(f'Loaded {len(tools)} tools from {mcp_url}')

    backend = FilesystemBackend(root_dir=HERE, virtual_mode=False)  # skills are read from disk
    log(f'Using model: {model}')
    report_skills(backend)
    agent = create_deep_agent(
        model=model,
        tools=tools,
        system_prompt=SYSTEM_PROMPT,
        backend=backend,
        skills=SKILL_SOURCES,
        response_format=Analysis,
    )
    # The model does not know today's date: give it, so "yesterday" or "this morning" resolve to real times.
    question = f'Current local time: {datetime.now().strftime(TIME_FORMAT)}\n\n{prompt}'
    result = await agent.ainvoke({'messages': [{'role': 'user', 'content': question}]})
    return result['structured_response']


def overall_severity(analysis: Analysis) -> str:
    severities = [f.severity for f in analysis.findings]
    severities.append({'critical': 'critical', 'degraded': 'warning'}.get(analysis.verdict, 'info'))
    return max(severities, key=SEVERITY_ORDER.__getitem__)


# ----------------------------------------------------------------------------- ServiceNow

def build_incident(analysis: Analysis) -> dict:
    instance, start, end = analysis.instance, analysis.window_start, analysis.window_end
    lines = [analysis.summary, '']
    for i, f in enumerate(analysis.findings, 1):
        lines += [f'{i}. [{f.severity.upper()}] {f.title} ({f.category})',
                  f'   Evidence: {f.evidence}',
                  f'   Recommendation: {f.recommendation}']
    lines.append(f'\nSource: Datasentinel — instance {instance}, {start} to {end} (platform local time).')
    level = IMPACT_URGENCY[overall_severity(analysis)]
    record = {
        'short_description': f'[Datasentinel] {analysis.verdict.capitalize()} workload on {instance} — {start[:10]}',
        'description': '\n'.join(lines),
        'work_notes': analysis.report_markdown,
        'impact': level,
        'urgency': level,
        'category': 'Database',
        'correlation_id': correlation_id(instance, start, end),
        'correlation_display': 'Datasentinel',
    }
    for field, env in (('assignment_group', 'SERVICENOW_ASSIGNMENT_GROUP'), ('caller_id', 'SERVICENOW_CALLER_ID')):
        if os.environ.get(env):
            record[field] = os.environ[env]
    return record


def correlation_id(instance: str, start: str, end: str) -> str:
    """Stable for a given instance and window, whatever the prompt's wording: minute precision, digits only."""
    compact = lambda t: ''.join(c for c in t if c.isdigit())[:12]  # noqa: E731
    return f'datasentinel:{instance}:{compact(start)}-{compact(end)}'


def upsert_incident(record: dict, transport: httpx.BaseTransport | None = None) -> tuple[str, dict]:
    """Add a work note to the open incident with the same correlation_id, or create one."""
    with httpx.Client(base_url=f'https://{os.environ["SERVICENOW_INSTANCE"]}', timeout=30.0, transport=transport,
                      auth=(os.environ['SERVICENOW_USER'], os.environ['SERVICENOW_PASSWORD']),
                      params={'sysparm_input_display_value': 'true'}) as snow:
        found = snow.get('/api/now/table/incident', params={
            'sysparm_query': f'correlation_id={record["correlation_id"]}^active=true',
            'sysparm_limit': 1, 'sysparm_fields': 'sys_id,number'})
        found.raise_for_status()
        rows = found.json()['result']
        if rows:
            note = f'Datasentinel re-analysis {datetime.now():%Y-%m-%d %H:%M}\n\n{record["description"]}'
            response = snow.patch(f'/api/now/table/incident/{rows[0]["sys_id"]}', json={'work_notes': note})
            action = 'updated'
        else:
            response = snow.post('/api/now/table/incident', json=record)
            action = 'created'
        response.raise_for_status()
        return action, response.json()['result']


# ----------------------------------------------------------------------------- main

def main(argv=None) -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('prompt', nargs='+', help='what to analyze, e.g. "analyze yesterday\'s workload on pg-crm"')
    parser.add_argument('--min-severity', choices=('warning', 'critical'), default='warning',
                        help='open a ticket only from this severity (default warning)')
    parser.add_argument('--dry-run', action='store_true', help='print the incident, send nothing to ServiceNow')
    parser.add_argument('--out', help='also write the analysis JSON to this file')
    args = parser.parse_args(argv)

    prompt = ' '.join(args.prompt)
    log('Datasentinel agent starting (non-interactive)')
    log(f'Question: {prompt}')
    analysis = asyncio.run(analyze(prompt))
    severity = overall_severity(analysis)
    if args.out:
        Path(args.out).write_text(json.dumps({**analysis.model_dump(), 'overall_severity': severity}, indent=2, ensure_ascii=False))
    log(f'{analysis.instance}, {analysis.window_start} to {analysis.window_end}: verdict {analysis.verdict}, '
        f'severity {severity}, {len(analysis.findings)} finding(s)')

    if SEVERITY_ORDER[severity] < SEVERITY_ORDER[args.min_severity]:
        log('Below --min-severity: nothing to report')
        return 0
    record = build_incident(analysis)
    if args.dry_run:
        print(json.dumps(record, indent=2, ensure_ascii=False))
        return 0
    action, incident = upsert_incident(record)
    print(json.dumps({'action': action, 'number': incident.get('number'), 'severity': severity}))
    return 0


if __name__ == '__main__':
    sys.exit(main())
```

Notes:

* The agent runs non-interactively. The system prompt forbids questions and tells it to follow the Skill procedure that matches the prompt. When the prompt names no instance, the agent analyzes the most loaded one. When it names no window, it analyzes yesterday. The agent chooses its own tool calls.
* The system prompt cancels the Skill's closing menu explicitly. The Skill normally ends every result with a numbered "What would you like to do next?" menu, which would land in the incident's work notes. The Skill exempts a non-interactive run that the host declares, and the last paragraph of `SYSTEM_PROMPT` is that declaration. The follow-ups the menu would have offered are written into the report as sentences instead.
* `skills=["./skills/"]` points at a parent directory, as in [Build a Python agent with deepagents](/manual/ai-connector/developers/build-a-python-agent-with-deepagents.md). The script warns on stderr when `skills/datasentinel/SKILL.md` is missing.
* The user token travels as the `Authorization: Bearer` header of the MCP connection. The model never sees it. See [How authentication works](/manual/ai-connector/server-setup/how-authentication-works.md#scripts-and-scheduled-jobs).
* `AGENT_MODEL` accepts any model LangChain supports, for example a local Gemma 4 through Ollama (`ollama:gemma4:e4b`, with `pip install langchain-ollama`). The model must support tool calling.
* To send the result somewhere else, replace `upsert_incident`. The analysis is already structured.

## Run it

Start with a dry run. The analysis runs end to end and the script prints the incident it **would** create. Nothing is sent to ServiceNow.

```bash
python servicenow_workload_analysis.py --dry-run --out yesterday.json \
    "Analyze yesterday's workload on pg-crm@srv1:5432"
```

The script first prints a header on stderr, like the [deepagents agent](/manual/ai-connector/developers/build-a-python-agent-with-deepagents.md). It tells you which MCP server, tools, model and Skill the run uses. The last line gives the instance and window the agent analyzed and the verdict:

```
Datasentinel agent starting (non-interactive)
Question: Analyze yesterday's workload on pg-crm@srv1:5432
Using MCP server: https://ds.example.com/mcp
Loaded 24 tools from https://ds.example.com/mcp
Using model: anthropic:claude-sonnet-5
Loaded skill: datasentinel - Inspect, troubleshoot, or report on the user's real PostgreSQL databases via...
pg-crm@srv1:5432, 2026-09-16 00:00:00 to 2026-09-16 23:59:59: verdict degraded, severity warning, 3 finding(s)
```

`Minted a new Datasentinel user-token in ~/.datasentinel_token` appears when the cached token was missing or expired. A `WARNING: no skills loaded from ./skills/` line means the Skill was not extracted next to the script: the agent would then run without it. Only the incident (or, after a real run, its number) goes to stdout.

The prompt is the last argument. Quote it, or the shell splits it into words (the script joins them back). Name the instance as Datasentinel shows it. Times are in the platform's local time. Any request the Skill covers works, for example:

```bash
python servicenow_workload_analysis.py \
    "Lock contention on pg-crm@srv1:5432 on 2026-09-16 between 02:00 and 04:00"

python servicenow_workload_analysis.py \
    "Check I/O pressure and the top queries on pg-erp@srv1:5433 this morning"
```

Other options:

| Option                                           | Effect                                                                                                      |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `--min-severity warning` (default) or `critical` | the lowest overall severity that opens a ticket. Below it the script prints "nothing to report" and exits 0 |
| `--out <file>`                                   | write the full analysis as JSON, for archiving or a second channel                                          |
| `--dry-run`                                      | print the incident, send nothing to ServiceNow                                                              |

Once the dry run looks right, schedule it:

```bash
# crontab: every morning at 06:15, one JSON per day next to the run log
15 6 * * * cd /opt/datasentinel && /usr/bin/python3 servicenow_workload_analysis.py --out /var/log/ds-snow/$(date +\%F).json "Analyze yesterday's workload on pg-crm@srv1:5432" >> /var/log/ds-snow/run.log 2>&1
```

## What the incident looks like

* **Short description:** `[Datasentinel] Degraded workload on pg-crm@srv1:5432 — 2026-09-16`
* **Description:** the plain-language summary, then one numbered entry per finding with its severity, category, evidence and recommendation.
* **Work notes:** the Markdown report in the Skill's shape.
* **Impact / urgency:** 2 / 2 for a critical severity, 3 / 3 for a warning.
* **Category:** `Database`. **Assignment group, caller:** from the environment variables above.
* **Correlation id:** `datasentinel:<instance>:<window>`. **Correlation display:** `Datasentinel`.

A second run for the same instance and window, whatever the wording of the prompt, finds the open incident by its correlation id and appends a work note. Closing the incident in ServiceNow is a human decision. A later run for the same window opens a new one only if none is active.

The script exits 0 when it is done, including "nothing to report", and 2 when the prompt is missing. Any other error exits 1 with its traceback, so your scheduler's monitoring sees it.

{% hint style="info" %}
The script authenticates to the MCP server with the 24-hour user token as a bearer header, like every scripted use of AI Connector. It caches the token in `~/.datasentinel_token` with permissions `0600`. It mints a new one from `DATASENTINEL_USER` / `DATASENTINEL_PASSWORD` when the cached one is missing or about to expire, so an unattended run never fails on an expired credential.
{% endhint %}

{% content-ref url="/pages/tYYPnYXT7D6UVZO3k9HD" %}
[Refresh the user token automatically](/manual/ai-connector/developers/refresh-user-token.md)
{% endcontent-ref %}

{% content-ref url="/pages/dd3mIsL8zkRvGeB54Umv" %}
[Investigate a blocking scenario](/manual/ai-connector/analyze-and-tune-workloads/investigate-blocking-scenarios.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.datasentinel.io/manual/ai-connector/developers/analyze-and-open-a-servicenow-ticket.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
