Task: workarena.servicenow.create-change-request
Goal: Create a new change request with a value of "Worked" for field "Close notes", a value of "CHG0000026" for field "Number", a value of "Moderate" for field "Risk", a value of "Resend the complete BGP table to neighboring routers--Both neighbors need to support soft reset route refresh capability.--Stores complete BGP table of you neighbor in router memory.--Not a good idea on a peering router with full feed, due to the memory requirements." for field "Description", a value of "-- Place router into maintenance mode in the monitoring platform-- Logon to router through SSH-- Run the following commandrouter(config-router)#router bgp 12345router(config-router)#neighbor {neighbor ip} soft-reconfig [inbound]router#clear ip bgp {neighbor ip} soft in-- Confirm the sessions have been cleared-- Place router back into operational mode in the monitoring platform" for field "Implementation plan", and a value of "Network" for field "Assignment group".
Task configuration: {'table_name': 'change_request', 'task_fields': ['close_notes', 'number', 'risk', 'description', 'implementation_plan', 'assignment_group'], 'template_record': {'active': 'false', 'activity_due': 'UNKNOWN', 'additional_assignee_list': '', 'approval': 'Approved', 'approval_history': '', 'approval_set': '', 'assigned_to': '', 'assignment_group': 'Network', 'backout_plan': 'Due to the limited number of commands in the implementation plan it is not possible to backout the change.If required you are authorized to reboot the router if BGP fails to work', 'business_duration': '', 'business_service': '', 'cab_date_time': '', 'cab_delegate': '', 'cab_recommendation': '', 'cab_required': 'false', 'calendar_duration': '', 'category': 'Other', 'change_plan': '', 'chg_model': '', 'close_code': 'Successful', 'close_notes': 'Worked', 'closed_at': '2015-07-06 12:00:51', 'closed_by': {'display_value': 'System Administrator', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user/6816f79cc0a8016401c5a33be04be441'}, 'cmdb_ci': '', 'comments': '', 'comments_and_work_notes': '', 'company': '', 'conflict_last_run': '', 'conflict_status': 'Not Run', 'contact_type': 'Phone', 'contract': '', 'correlation_display': '', 'correlation_id': '', 'delivery_plan': '', 'delivery_task': '', 'description': 'Resend the complete BGP table to neighboring routers--Both neighbors need to support soft reset route refresh capability.--Stores complete BGP table of you neighbor in router memory.--Not a good idea on a peering router with full feed, due to the memory requirements.', 'due_date': '', 'end_date': '', 'escalation': 'Normal', 'expected_start': '', 'follow_up': '', 'group_list': '', 'impact': '3 - Low', 'implementation_plan': '-- Place router into maintenance mode in the monitoring platform-- Logon to router through SSH-- Run the following commandrouter(config-router)#router bgp 12345router(config-router)#neighbor {neighbor ip} soft-reconfig [inbound]router#clear ip bgp {neighbor ip} soft in-- Confirm the sessions have been cleared-- Place router back into operational mode in the monitoring platform', 'justification': 'Glyptotherium phoenix stormwind opinionately middlemanism', 'knowledge': 'false', 'location': '', 'made_sla': 'true', 'number': 'CHG0000026', 'on_hold': 'false', 'on_hold_reason': '', 'on_hold_task': '', 'opened_at': '2015-07-06 12:00:26', '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': 'Requested', 'phase_state': 'Open', 'priority': '4 - Low', 'production_system': 'false', 'reason': None, 'reassignment_count': '2', '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': 'Moderate', 'risk_impact_analysis': 'unringed irene unexpressable rowdyishly pilifer', 'route_reason': '', 'scope': 'Medium', 'service_offering': '', 'short_description': 'Clear BGP sessions on a Cisco router', 'sla_due': 'UNKNOWN', 'start_date': '', 'state': 'Closed', 'std_change_producer_version': {'display_value': 'Clear BGP sessions on a Cisco router - 1', 'link': 'https://dev215901.service-now.com/api/now/table/std_change_producer_version/16c2273c47010200e90d87e8dee49006'}, 'sys_class_name': 'Change Request', 'sys_created_by': 'admin', 'sys_created_on': '2015-07-06 12:00:26', 'sys_domain': {'display_value': 'global', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user_group/global'}, 'sys_domain_path': '/', 'sys_id': '7b77f5de47410200e90d87e8dee490b8', 'sys_mod_count': '8', 'sys_tags': '', 'sys_updated_by': 'admin', 'sys_updated_on': '2015-07-06 12:00:51', 'task_effective_number': 'CHG0000026', 'test_plan': '--Confirm that there are no monitoring alerts for the router', 'time_worked': '', 'type': 'Standard', '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': '2015-07-06 12:00:42', 'work_notes': '', 'work_notes_list': '', 'work_start': '2015-07-06 12:00:37'}}

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