Task: workarena.servicenow.create-hardware-asset
Goal: Create a new hardware asset with a value of "Computer" for field "Model category", a value of "Apple MacBook Air 13"" for field "Model", a value of "Apple" for field "Vendor", a value of "SN-43d16b9d-ddc1-4150-b0dd-039a1b69a45f" for field "Serial number", a value of "P1000639" for field "Asset tag", a value of "" for field "Owned by", a value of "Sales" for field "Department", a value of "" for field "Support group", a value of "30 Katharinenstr, Hamburg" for field "Location", a value of "" for field "Supported by", a value of "Rental" for field "Acquisition method", a value of "" for field "Beneficiary", a value of "" for field "Parent", a value of "caeca crevalle understream metallogenic spitscocked" for field "Comments", a value of "relatch unblinking endognath trestlewise" for field "Invoice number", a value of "" for field "Request line", a value of "folkway storkling introsuction Dawsonia pleadingness" for field "Disposal reason", a value of "ACME Germany" for field "Company", a value of "Sales" for field "Cost center", a value of "Nyroca Herodiones toppingly behatted bor" for field "GL account", a value of "conventical epocha somal groceryman spin" for field "Lease contract", a value of "SL 5 Years" for field "Depreciation", a value of "Capex" for field "Expenditure type", a value of "" for field "Managed by", and a value of "Primary" for field "Asset function".
Task configuration: {'table_name': 'alm_hardware', 'task_fields': ['model_category', 'model', 'vendor', 'serial_number', 'asset_tag', 'owned_by', 'department', 'support_group', 'location', 'supported_by', 'acquisition_method', 'beneficiary', 'parent', 'comments', 'invoice_number', 'request_line', 'disposal_reason', 'company', 'cost_center', 'gl_account', 'lease_id', 'depreciation', 'expenditure_type', 'managed_by', 'asset_function'], 'template_record': {'acquisition_method': 'Rental', 'asset_function': 'Primary', 'asset_tag': 'P1000639', 'assigned': '2022-05-04 00:00:00', 'assigned_to': 'Isabell Armout', 'beneficiary': '', 'checked_in': '', 'checked_out': '', 'ci': {'display_value': 'MacBook Air 13"', 'link': 'https://dev215901.service-now.com/api/now/table/cmdb_ci/a4a9680d3790200044e0bfc8bcbe5d9d'}, 'comments': 'caeca crevalle understream metallogenic spitscocked', 'company': 'ACME Germany', 'cost': '$1,599.99', 'cost_center': 'Sales', 'delivery_date': '2022-01-27 23:00:00', 'department': 'Sales', 'depreciated_amount': '$624.75', 'depreciation': 'SL 5 Years', 'depreciation_date': '2022-03-29 00:00:00', 'display_name': 'P1000639 - Apple MacBook Air 13"', 'disposal_reason': 'folkway storkling introsuction Dawsonia pleadingness', 'due': '', 'due_in': None, 'eligible_for_refresh': 'false', 'expenditure_type': 'Capex', 'gl_account': 'Nyroca Herodiones toppingly behatted bor', 'install_date': '2022-03-28 00:00:00', 'install_status': 'In use', 'invoice_number': 'relatch unblinking endognath trestlewise', 'justification': '', 'lease_id': 'conventical epocha somal groceryman spin', 'life_cycle_stage': '', 'life_cycle_stage_status': '', 'location': '30 Katharinenstr, Hamburg', 'managed_by': '', 'model': 'Apple MacBook Air 13"', 'model_category': 'Computer', 'old_status': '', 'old_substatus': '', 'order_date': '2021-12-30 00:00:00', 'owned_by': '', 'parent': '', 'po_number': 'PO100008', 'pre_allocated': 'false', 'purchase_date': '2022-01-13', 'quantity': '1', 'request_line': '', 'resale_price': '$0.00', 'reserved_for': '', 'residual': '$975.24', 'residual_date': '2024-03-11', 'resold_value': '$0.00', 'retired': '', 'retirement_date': '', 'salvage_value': '$0.00', 'serial_number': 'SN-43d16b9d-ddc1-4150-b0dd-039a1b69a45f', 'skip_sync': 'false', 'stockroom': '', 'substatus': None, 'support_group': '', 'supported_by': '', 'sys_class_name': 'Hardware', 'sys_created_by': 'admin', 'sys_created_on': '2022-12-30 00:13:17', 'sys_domain': {'display_value': 'global', 'link': 'https://dev215901.service-now.com/api/now/table/sys_user_group/global'}, 'sys_domain_path': '/', 'sys_id': '98a9680d3790200044e0bfc8bcbe5d9c', 'sys_mod_count': '11', 'sys_tags': '', 'sys_updated_by': 'system', 'sys_updated_on': '2024-03-11 07:39:02', 'vendor': 'Apple', 'warranty_expiration': '2025-03-27', 'work_notes': ''}}

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