Task: workarena.servicenow.create-change-request
Goal: Create a new change request with a value of "Update /etc/network/interfaces to include name servers 8.8.8.8 & 8.8.4.4" for field "Short description", a value of "Unsuccessful" for field "Close code", a value of "CHG0000093" for field "Number", a value of "High" for field "Risk", a value of "" for field "Service", a value of "Network" for field "Category", a value of "millerole amphigenously nephrocolic corticipetal Patripassian" for field "Close notes", a value of "PS LinuxApp01" for field "Configuration item", a value of "We need to add additional redundant name servers to lnux100, lnux101 and PS LinuxApp01" for field "Description", a value of "vi /etc/network/interfacesinsert line:dns-nameservers 8.8.8.8 8.8.4.4:wqtest name resolution using command: host www.google.com" for field "Implementation plan", a value of "azomethine neglectproof flawlessness constableship frisure" for field "Justification", a value of "" for field "Service offering", a value of "2 - Medium" for field "Impact", a value of "Hardware" for field "Assignment group", and a value of "pedanticly toxalbumic hyphomycete soundheadedness terrier" for field "Test plan".
Task configuration: {'table_name': 'change_request', 'task_fields': ['short_description', 'close_code', 'number', 'risk', 'business_service', 'category', 'close_notes', 'cmdb_ci', 'description', 'implementation_plan', 'justification', 'service_offering', 'impact', 'assignment_group', 'test_plan'], 'template_record': {'active': 'true', 'activity_due': 'UNKNOWN', 'additional_assignee_list': '', 'approval': 'Requested', 'approval_history': '2023-10-08 06:15:18 - System Administrator (Approval history)\nGroup approval for Hardware approved by user David Loo (Technical approvals).\n\n', 'approval_set': '', 'assigned_to': {'display_value': 'Bow Ruggeri', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user/f298d2d2c611227b0106c6be7f154bc8'}, 'assignment_group': 'Hardware', 'backout_plan': 'Comment out new line in /etc/network/interfaces', 'business_duration': '', 'business_service': '', 'cab_date_time': '', 'cab_delegate': '', 'cab_recommendation': '', 'cab_required': 'false', 'calendar_duration': '', 'category': 'Network', 'change_plan': '', 'chg_model': '', 'close_code': 'Unsuccessful', 'close_notes': 'millerole amphigenously nephrocolic corticipetal Patripassian', 'closed_at': '', 'closed_by': '', 'cmdb_ci': 'PS LinuxApp01', 'comments': '', 'comments_and_work_notes': '', 'company': '', 'conflict_last_run': '2023-10-09 01:54:20', 'conflict_status': 'Conflict', 'contact_type': None, 'contract': '', 'correlation_display': '', 'correlation_id': '', 'delivery_plan': '', 'delivery_task': '', 'description': 'We need to add additional redundant name servers to lnux100, lnux101 and PS LinuxApp01', 'due_date': '', 'end_date': '2024-01-02 18:45:00', 'escalation': 'Normal', 'expected_start': '', 'follow_up': '', 'group_list': '', 'impact': '2 - Medium', 'implementation_plan': 'vi /etc/network/interfacesinsert line:dns-nameservers 8.8.8.8 8.8.4.4:wqtest name resolution using command: host www.google.com', 'justification': 'azomethine neglectproof flawlessness constableship frisure', 'knowledge': 'false', 'location': '', 'made_sla': 'true', 'number': 'CHG0000093', 'on_hold': 'false', 'on_hold_reason': '', 'on_hold_task': '', 'opened_at': '2023-10-06 01:21:37', 'opened_by': {'display_value': 'System Administrator', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user/6816f79cc0a8016401c5a33be04be441'}, 'order': '', 'outside_maintenance_schedule': 'false', 'parent': '', 'phase': None, 'phase_state': 'Open', 'priority': '3 - Moderate', 'production_system': 'false', 'reason': None, 'reassignment_count': '0', 'requested_by': {'display_value': 'System Administrator', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user/6816f79cc0a8016401c5a33be04be441'}, 'requested_by_date': '', 'review_comments': '', 'review_date': '', 'review_status': '', 'risk': 'High', 'risk_impact_analysis': 'disciplinant shirtband carbene cotheorist mistressdom', 'route_reason': '', 'scope': 'Medium', 'service_offering': '', 'short_description': 'Update /etc/network/interfaces to include name servers 8.8.8.8 & 8.8.4.4', 'sla_due': 'UNKNOWN', 'start_date': '2024-01-02 18:30:00', 'state': 'Authorize', 'std_change_producer_version': '', 'sys_class_name': 'Change Request', 'sys_created_by': 'admin', 'sys_created_on': '2023-10-06 01:21:37', 'sys_domain': {'display_value': 'global', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user_group/global'}, 'sys_domain_path': '/', 'sys_id': 'eecdf552db252200a6a2b31be0b8f539', 'sys_mod_count': '23', 'sys_tags': '', 'sys_updated_by': 'admin', 'sys_updated_on': '2023-10-09 01:54:20', 'task_effective_number': 'CHG0000093', 'test_plan': 'pedanticly toxalbumic hyphomycete soundheadedness terrier', 'time_worked': '', 'type': 'Normal', 'unauthorized': 'false', 'universal_request': '', 'upon_approval': 'Proceed to Next Task', 'upon_reject': 'Cancel all future Tasks', 'urgency': '3 - Low', 'user_input': '', 'watch_list': '', 'work_end': '', 'work_notes': '', 'work_notes_list': '', 'work_start': '2023-10-08 02:48:26'}}

Validation code:
import json
import os
import re
import urllib.parse as parse
from typing import Any, Dict, List

import requests


def validate(messages: List[Dict[str, str]], config: Dict[str, Any]) -> int:
    """
    Validate that a record was created with expected field values.
    Returns 1 on success, else 0.

    Expected config keys:
      - task_fields (list[str])                  # fields to validate
      - template_record (dict)                   # expected display values for those fields
    """

    # Check whether we have the necessary information to validate
    print(config)
    if "table_name" not in config or "task_fields" not in config or "template_record" not in config:
        raise ValueError("Target must contain 'table_name', 'task_fields' and 'template_record'")

    # Check whether we have messages
    if not messages:
        raise ValueError("Messages must not be empty")

    # Extract last message content
    content = messages[-1].get("content", "")

    # Extract all URLs — markdown links first, then bare URLs
    seen = set()
    urls = []
    for m in re.finditer(r"\[.*?\]\((https?://[^)]+)\)", content):
        url = m.group(1)
        if url not in seen:
            seen.add(url)
            urls.append(url)
    for m in re.finditer(r"https?://\S+", content):
        url = m.group(0).rstrip(")].,")
        if url not in seen:
            seen.add(url)
            urls.append(url)
    if not urls:
        return 0

    def extract_sys_id(url: str) -> str | None:
        """
        Return the 32-hex sys_id from a ServiceNow URL, or None if not found.
        Handles encoded target URLs like .../params/target/incident.do%3Fsys_id%3D<id>
        """
        # Unquote repeatedly (some SN URLs are doubly-encoded)
        for _ in range(3):
            new = parse.unquote(url)
            if new == url:
                break
            url = new

        m = re.search(r"(?:[?&])sys_id=([0-9a-f]{32})", url, re.IGNORECASE)
        return m.group(1) if m else None

    sys_id = None
    for raw_url in urls:
        current_url = parse.urlunparse(parse.urlparse(parse.unquote(raw_url)))
        sys_id = extract_sys_id(current_url)
        if sys_id:
            break
    if not sys_id:
        return 0

    # Query instance to get record
    response = requests.request(
        method="GET",
        url=os.getenv("SERVICENOW_INSTANCE_URL", "").rstrip("/") + f"/api/now/table/{config['table_name']}",
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json",
            **json.loads(os.getenv("SERVICENOW_EXTRA_HTTP_HEADERS", "{}")),
        },
        params={
            "sysparm_query": f"sys_id={sys_id}",
            "sysparm_display_value": "true",
            "sysparm_fields": ",".join(config["task_fields"]),
        },
    )
    response.raise_for_status()
    if type(response) == dict:
        record = response
    else:
        record = response.json()

    if "result" in record:
        record = record["result"]

    if isinstance(record, list):
        if len(record) == 0:
            return 0
        record = record[0]

    # Check that the record matches the expected values
    for f in config["task_fields"]:
        val = record.get(f)
        # Handle nested display_value dicts from the API
        if isinstance(val, dict) and "display_value" in val:
            val = val["display_value"]
        if val is None:
            return 0
        if val != config["template_record"][f]:
            return 0
    return 1