{
  "entries": [
    {
      "terms": ["payment", "payments", "pay", "paid", "money", "earnings"],
      "targets": ["get_account_current_balance", "get_account_payable_balance"],
      "related": [
        "get_account_payee",
        "get_account_payment_hold",
        "get_ledger_reserve_releases"
      ],
      "domain": "ledger",
      "priority": 2,
      "context": "Use current_balance for total accumulated earnings including unprocessed periods; use payable_balance for the amount actually eligible for payment after holds, reserves, and thresholds are applied. Both require account_id.",
      "gotchas": [
        "current_balance includes unprocessed periods; payable_balance does not",
        "payable_balance is broken down per contract, so summing gives the account-level payable",
        "Payment disbursement history (when money was sent, bank transfer details) is NOT available — direct the user to the Payments section of the platform",
        "Payment holds (get_account_payment_hold), payable balance (get_account_payable_balance), and payment entity status (get_statement_period_payment_entities / get_statement_period_payment_entity_states) ARE available",
        "Stream counts / play quantities are NOT available from the financial service — it only returns money earned; use snowflake_explore_skill for per-platform stream counts"
      ],
      "businessRules": [
        "Payment eligibility requires ALL of: (1) current_balance >= account payment_minimum, (2) current_balance >= currency-level minimum, (3) payment_eligibility state = approved, (4) tax_eligibility state = complete, (5) no blocking prior payment in-flight, (6) no active payment hold, (7) payoneer_program_id and payoneer_payee_id both set, (8) group criteria match (currency, entity, schedule)",
        "Payment blocking is compound: active hold OR missing payee OR incomplete tax forms OR below minimum threshold — each independently blocks payment",
        "Payment schedules: 30/45/60/90 days after month-end, quarter-end, or half-year-end, plus TEMPORARY (13 values total)"
      ],
      "relationships": [
        "Account -> Payee (1:1, required for payment eligibility)",
        "Account -> PaymentHold (0..1, blocks all disbursements when active)",
        "Account -> TaxInfo (1:1, required for withholding calculation)"
      ]
    },
    {
      "terms": ["balance", "account balance", "ledger balance", "owed"],
      "targets": [
        "get_account_current_balance",
        "get_account_payable_balance",
        "get_account_ledger_contracts"
      ],
      "related": [
        "get_pending_ledger_adjustments",
        "get_ledger_reserve_releases"
      ],
      "domain": "ledger",
      "priority": 2,
      "context": "Returns earned revenue, advances, adjustments, and reserves. Use get_account_ledger_contracts to list all contracts with ledger entries for an account. All require account_id.",
      "gotchas": [
        "An account may have ledger entries across many contracts; get_account_ledger_contracts enumerates them",
        "Two separate adjustment type enumerations exist: ledger reference types vs. import adjustment types — they are not interchangeable"
      ],
      "businessRules": [
        "Balance = earned revenue + advances + adjustments − reserves",
        "Payable balance sums only POSITIVE contract-level balances — negative contract balances do NOT reduce the account-level payable",
        "Legacy Workstation data backfills when ledger events are missing (hybrid migration — Moneyhub falls back to WorkstationSummary)"
      ]
    },
    {
      "terms": ["revenue", "sales", "earnings breakdown", "income"],
      "targets": [
        "get_account_revenue_by_store",
        "get_account_revenue_by_artist",
        "get_account_revenue_by_transaction_type",
        "revenue_overview_skill"
      ],
      "related": [
        "get_account_revenue_by_subaccount",
        "get_account_statement_periods"
      ],
      "domain": "moneyhub",
      "priority": 2,
      "context": "Revenue can be sliced by DSP/store, by artist, or by transaction type. Use revenue_overview_skill for a combined view of periods, stores, and artists in one call. All require account_id with optional period range filters.",
      "gotchas": [
        "revenue_overview_skill combines multiple breakdowns in a single call and is preferred over calling individual tools separately",
        "get_account_revenue_by_subaccount is only meaningful for vendor-type accounts",
        "Default result limits vary by endpoint: revenue_by_store=5, revenue_by_artist=50, revenue_by_subaccount=50",
        "get_account_revenue_by_store defaults to limit=5; pass limit=100 when the user asks for a full store breakdown or 'all stores'; when showing a subset, say 'Top N stores' — never present a partial list as the complete picture",
        "When the response includes a totals object, use it for the account-level total rather than manually summing individual items (which may be paginated and incomplete)",
        "Never present a per-store or per-artist figure as the account total — always distinguish store-level, artist-level, and account-level totals"
      ],
      "businessRules": [
        "All revenue amounts are returned in account_payee_currency, not the original transaction currency",
        "Revenue fields: gross_revenue_payee_currency (before fees) vs. net_revenue_payee_currency (after fees); subaccounts also include mechanical_deduction_amount and publisher_admin_fee",
        "Always report both gross and net revenue when the data includes both fields; gross is before distribution fees, net is after — always label which is which",
        "Always include the currency code (account_payee_currency) with every financial figure — never present a number without it",
        "Always include the time period: use statement_period_name for specific periods (e.g. '2024-01'), or say 'all-time' / state the date range — never silently sum across periods"
      ]
    },
    {
      "terms": ["streaming revenue", "downloads", "transaction type"],
      "targets": ["get_account_revenue_by_transaction_type"],
      "related": ["get_account_revenue_by_store"],
      "domain": "moneyhub",
      "priority": 2,
      "context": "Breaks down REVENUE by type: streaming, downloads, physical, sync, neighboring rights, and mechanicals. Requires account_id. Use this to understand which revenue streams contribute most. This tool returns revenue amounts and transaction quantity per statement period — NOT raw stream counts or daily analytics.",
      "businessRules": [
        "Transaction types include streaming, downloads, physical, sync, neighboring rights, and mechanicals"
      ],
      "gotchas": [
        "This returns REVENUE data (dollars), not stream COUNTS — for track-level stream counts, use graphql_explore_skill or snowflake_explore_skill instead",
        "Cannot filter by track ISRC or specific date range — only by account_id and statement period"
      ]
    },
    {
      "terms": [
        "streams",
        "streaming",
        "stream count",
        "total streams",
        "track streams",
        "daily streams",
        "how many streams",
        "song streams",
        "artist streams",
        "plays",
        "play count",
        "listens"
      ],
      "targets": [
        "graphql_explore_skill",
        "query_graphql",
        "snowflake_explore_skill",
        "query_snowflake"
      ],
      "related": ["resolve_artist_skill", "get_products_by_isrc"],
      "domain": "analytics",
      "priority": 2,
      "context": "For track-level streaming analytics (stream counts, not revenue), use GraphQL or Snowflake. GraphQL path: (1) search track by name with globalSoundRecordingSearchES to get ISRC, (2) query globalSoundRecordingByIsrc with analytics.timeseries(startDate, endDate) for daily streams. Snowflake path: query V_STREAMS_BY_TRACK_FEED_DISTRIBUTOR_DAILY filtering by isrc and download_activity_date range. To resolve an artist name first, use resolve_artist_skill or globalParticipantSearchES.",
      "gotchas": [
        "Do NOT use get_account_revenue_by_transaction_type for stream counts — it returns revenue amounts, not stream volume",
        "Stream data is keyed by ISRC (track) and global_participant_id (artist), not by account_id — resolve the track/artist first",
        "Snowflake stream tables use feed_id for store filtering (e.g., Spotify=286); join to DIM_FEED for store names"
      ],
      "businessRules": [
        "Snowflake stream tables: V_STREAMS_BY_TRACK_FEED_DISTRIBUTOR_DAILY (by ISRC), V_STREAMS_BY_PARTICIPANT_TRACK_FEED_DISTRIBUTOR_DAILY (by artist+track), V_STREAMS_BY_TRACK_COUNTRY_FEED_DISTRIBUTOR_DAILY (by track+country)",
        "GraphQL analytics: globalSoundRecordingByIsrc.analytics.timeseries() returns daily streams, skipRate, skips, saves"
      ]
    },
    {
      "terms": ["store", "platform", "DSP", "spotify", "apple music"],
      "targets": ["get_account_revenue_by_store"],
      "related": ["get_account_revenue_by_artist"],
      "domain": "moneyhub",
      "priority": 2,
      "context": "Shows revenue broken down by digital service provider (Spotify, Apple Music, Amazon, etc.). Requires account_id with optional period range. Use to compare performance across distribution platforms."
    },
    {
      "terms": [
        "artist",
        "artists",
        "performer",
        "band",
        "musician",
        "singer",
        "songwriter",
        "producer",
        "DJ",
        "rapper"
      ],
      "targets": ["resolve_artist_skill", "get_account_revenue_by_artist"],
      "related": ["account_overview_skill", "get_products_by_isrc"],
      "domain": "moneyhub",
      "priority": 2,
      "context": "Use resolve_artist_skill to convert an artist name into account IDs via the participant catalog; it returns matching accounts and known aliases. Use get_account_revenue_by_artist to see per-artist revenue once you have an account_id. Users often refer to accounts by artist name, not legal entity name — always resolve the artist first.",
      "gotchas": [
        "search_accounts will NOT match artist or stage names — always use resolve_artist_skill first to resolve artist names to account IDs",
        "Artist name → resolve_artist_skill (searches global participant catalog, returns account IDs via label associations where vendorId = account_id) → account_overview_skill or get_account_revenue_by_artist"
      ]
    },
    {
      "terms": ["contract", "deal", "agreement", "terms"],
      "targets": ["search_contracts", "contract_overview_skill"],
      "related": [
        "get_contract_exclusions",
        "find_contract_terms_by_attachment",
        "get_historical_advances",
        "query_graphql"
      ],
      "domain": "royalties",
      "priority": 2,
      "context": "Use search_contracts to find contracts by name or list all for an account. Use contract_overview_skill for a complete view of a specific contract including terms, advances, parties, lifecycle, and run status. search_contracts takes optional search_term and account_id; contract_overview_skill requires contract_id. contract_overview_skill returns enriched terms with full conditions arrays — each condition contains: contract_term_condition_name (human-readable label, may be null), term_rate (royalty rate 0-100), commission (term_rate + commission = 100), priority (lower = evaluated first), and a conditions object with countries, stores, and transaction_types arrays. Each transaction_types entry is resolved to { id, code, name } where code is the short code (e.g. 'AEV', 'AEA') and name is the full label (e.g. 'Ad-Enabled Video Streams').",
      "gotchas": [
        "contract_overview_skill requires a contract_id, not an account_id; use search_contracts first to find the contract_id",
        "Contract terms enrichment fetches transaction types in parallel (5 concurrent); may return partial data on access denial",
        "Contract lifecycle transitions (ACTIVE → TO_BE_TERMINATED → TERMINATED) are automated by Run Controller, not triggered by users",
        "When answering questions about specific transaction types (like 'AEV'), search through ALL terms and their conditions by matching the transaction type code or name inside conditions.transaction_types — match either the short code or the full name"
      ],
      "businessRules": [
        "Contract statuses: ACTIVE | INIT | IN_COLLECTION_PERIOD | TERMINATED | TO_BE_TERMINATED",
        "contractType: DISTRIBUTION | NEIGHBOURING_RIGHTS — neighbouring rights contracts have separate calculation rules",
        "Contract term types: artist, catalog, contribution, label, product, track, contributor_schedule, contribution_schedule — each has different rate calculation rules",
        "Term rate + commission are complements: commission = 100 − term_rate",
        "Default country exclusion: Russia (RUS) is excluded from all new contracts by default",
        "Flowthrough distribution on a contract can be ACTIVE, PAUSED, or SHUTOFF independently of contract status"
      ],
      "relationships": [
        "Contract -> Account (N:1, via accountId)",
        "Contract -> ContractTerms (1:N) -> Conditions (1:N) -> TransactionTypes (N:N)",
        "Contract -> ContractParties (1:N, contributors/labels linked to the contract)",
        "Contract -> ContractExclusions (1:N, territory or product carve-outs)",
        "Contract -> RunController (N:1, groups sibling contracts for batch processing)"
      ]
    },
    {
      "terms": ["advance", "advances", "recoupment", "recoup", "unrecouped"],
      "targets": ["get_historical_advances"],
      "related": [
        "get_account_current_balance",
        "search_contracts",
        "contract_overview_skill"
      ],
      "domain": "royalties",
      "priority": 2,
      "context": "Returns historical advance payment records for a contract: date paid, period, and payee currency amount. Requires contract_id. Check payable_balance to see unrecouped advance amounts affecting payment eligibility.",
      "businessRules": [
        "Advances must be fully recouped before royalties become payable on a contract",
        "Unrecouped advance balance is visible in payable_balance per-contract breakdown",
        "Advance milestones: contract_execution, delivery, fund_contingent, fund_non_contingent, option — each may have different approval requirements",
        "Advance statuses: NOT_QUALIFIED → QUALIFIED → APPROVED → PENDING_PAYMENT → PAID (can be DELETED at any stage)"
      ],
      "gotchas": [
        "Advance queries run across 7 status endpoints in parallel; partial results returned if some endpoints return access-denied",
        "Withholding tax and VAT on advances are calculated per country of tax residence and treaty status, not per advance"
      ]
    },
    {
      "terms": ["statement", "statement period", "accounting period", "period"],
      "targets": [
        "get_current_statement_period",
        "get_statement_periods",
        "get_recent_statement_periods"
      ],
      "related": [
        "get_upcoming_statement_periods",
        "get_accounting_periods",
        "get_account_statement_periods"
      ],
      "domain": "royalties",
      "priority": 2,
      "context": "Statement periods define the global accounting calendar. Use get_current_statement_period for the active period, get_recent_statement_periods for recently closed ones, and get_upcoming_statement_periods for future scheduled ones. None require parameters except get_statement_periods which accepts optional limit/offset.",
      "gotchas": [
        "Statement periods are global (not per-account); use get_account_statement_periods for per-account period revenue totals",
        "get_accounting_periods returns accounting periods which are a different granularity from statement periods",
        "Once a statement period is closed, no new adjustments can be added — apply all pending adjustments before closing",
        "statement_period_id has NO date meaning — never guess, calculate, or infer what a period ID maps to; the only authoritative mapping is a statement_period_name field returned by a tool",
        "If a tool response contains a statement_period_id without a corresponding _name field, call get_account_statement_periods or revenue_overview_skill to resolve it before presenting dates to the user"
      ],
      "businessRules": [
        "Accounting run full status machine: NO_ACTION_TAKEN → WAITING_TO_RUN → RUNNING → COMPLETE → COMMITTING → COMMITTED (or ERROR at any step; INVALID auto-creates a clone run)",
        "Statement periods are identified by sequential integer IDs (e.g., P316) that increment by 1 each month",
        "Statement period close preconditions (all 5 required): (1) status must be CURRENT, (2) all accounting periods CLOSED, (3) all balances closed, (4) all payment entities visible to customer, (5) next sequential period must exist"
      ],
      "relationships": [
        "StatementPeriod -> AccountingPeriod (1:N, one per contract_type: distribution, neighbouring_rights)",
        "AccountingPeriod -> AccountingRun (1:N, each run produces results for contracts in scope)",
        "AccountingRun -> RunController (N:1, groups sibling contracts for batch processing)"
      ]
    },
    {
      "terms": ["hold", "payment hold", "frozen", "blocked"],
      "targets": ["get_account_payment_hold"],
      "related": ["get_account_payment_hold_history"],
      "domain": "account",
      "priority": 2,
      "context": "Returns the active hold status and reason for an account. Requires account_id. Use get_account_payment_hold_history for the full audit trail of hold changes over time.",
      "businessRules": [
        "A payment hold blocks all disbursements for an account regardless of balance"
      ],
      "gotchas": [
        "Payment hold reason is free-text and must be audited; use get_account_payment_hold_history for the full audit trail",
        "Payment hold scheduling is non-obvious: is_on_hold=true + start_date=future means 'will be on hold starting that date'; is_on_hold=false + start_date=future means 'currently on hold UNTIL that date'"
      ]
    },
    {
      "terms": ["payee", "payee info", "banking", "bank details", "wire"],
      "targets": ["get_account_payee"],
      "related": ["get_account_payee_history", "get_account_tax_info_history"],
      "domain": "account",
      "priority": 2,
      "context": "Returns the payment method info (Payoneer) for an account: payee ID, name, and payment type. Requires account_id."
    },
    {
      "terms": ["tax", "tax info", "W-8", "W-9", "withholding"],
      "targets": ["get_account_tax_info_history"],
      "related": ["get_account_payee"],
      "domain": "account",
      "priority": 2,
      "context": "Returns tax information change history for an account including W-9/W-8 forms, VAT status, and withholding tax rates. Requires account_id.",
      "businessRules": [
        "Tax withholding rates affect net payable amounts; changes to tax info are tracked with full audit history"
      ]
    },
    {
      "terms": ["adjustment", "adjustments", "credit", "debit", "correction"],
      "targets": [
        "get_pending_ledger_adjustments",
        "get_account_ledger_adjustments"
      ],
      "related": [
        "get_moneyhub_ledger_adjustments",
        "get_adjustment_files",
        "get_reference_adjustment_types",
        "validate_adjustment_file",
        "submit_adjustment_batch",
        "upload_adjustment_file",
        "get_adjustment_template",
        "get_statement_period_for_adjustment",
        "get_adjustment_types"
      ],
      "domain": "ledger",
      "priority": 2,
      "context": "Manual corrections to ledger balances. Use get_pending_ledger_adjustments for corrections awaiting finalization in a statement period (requires statement_period_id). Use get_account_ledger_adjustments for applied/pending adjustments on an account with optional period and status filters (requires account_id). Use get_moneyhub_ledger_adjustments when you need amounts converted to payee currency.",
      "gotchas": [
        "get_pending_ledger_adjustments requires statement_period_id, not account_id",
        "get_moneyhub_ledger_adjustments returns the same adjustments as get_account_ledger_adjustments but converted to payee currency",
        "Two submission paths exist: file upload (Excel → S3 → Airflow) or generated (NDJSON → Lambda); always call validate_adjustment_file first for file uploads",
        "Comments are limited to 180 characters for both client-facing and internal fields",
        "'Account Expense' adjustment type is silently rejected by the import pipeline Lambda — never use it",
        "Flowthrough adjustments (type_id=105) do NOT aggregate — they retain contract_id for disambiguation, while all other types aggregate by type per period",
        "Before building any preview, verify: (1) exchange_rates_delivered on target period via get_statement_period_for_adjustment, (2) contract-account linkage exists, (3) currency matches account payee_currency",
        "Activity Month/Year = when the revenue event happened; Statement Month/Year = which accounting period to apply to — these differ for retroactive adjustments"
      ],
      "businessRules": [
        "95 valid adjustment types exist; use get_adjustment_types at runtime for the authoritative list — never generate 'Account Expense' (silently rejected by import pipeline). 56 currency codes are supported; default to the account's payee_currency and the handler validates at submission time",
        "All field values must be strings including numeric fields",
        "Amounts must include exactly 2 decimal places and can be negative",
        "If adjustment_type is 'Flowthrough', the apply_to_flowthrough_payment flag (Y/N) is required",
        "If distribution_type is set, UPC is required",
        "Never generate 'Account Expense' as an adjustment type — it is silently rejected by the import pipeline Lambda",
        "Transfer of Income pattern: query net revenue for source contract → calculate percentage → two rows (negative source, positive destination) → verify net is zero",
        "Flowthrough pattern: revenue by store → proportional shares of total → one row per store → Apply to Flowthrough Payment: Y",
        "Ambiguity handling: multiple open periods → list and ask; zero revenue → flag and ask; retroactive → ask both activity and statement dates; 'net' vs 'gross' → confirm which"
      ],
      "relationships": [
        "AdjustmentBatch -> StatementPeriod (N:1, defaults to current open period)",
        "AdjustmentRow -> Account + Contract (required) + UPC (conditional on distribution_type)"
      ]
    },
    {
      "terms": ["reserve", "reserves", "release schedule", "holdback"],
      "targets": ["get_ledger_reserve_releases"],
      "related": ["get_account_current_balance"],
      "domain": "ledger",
      "priority": 2,
      "context": "Shows amounts held in reserve and their scheduled release dates for a statement period. Requires statement_period_id. Reserves reduce the payable balance until they are released.",
      "businessRules": [
        "Reserves are deducted from payable balance and released on a schedule defined per contract"
      ]
    },
    {
      "terms": ["product", "release", "album", "single", "EP", "catalog"],
      "targets": ["get_product", "get_product_by_upc", "get_products_by_isrc"],
      "domain": "product",
      "priority": 2,
      "context": "Look up music releases (albums, singles, EPs). Use get_product with a product_id, get_product_by_upc with a UPC barcode, or get_products_by_isrc to find products containing a specific track recording. Returns name, UPC, release date, and status.",
      "gotchas": [
        "get_products_by_isrc returns all products containing that ISRC, which may span multiple releases or compilations",
        "vendor_id in the product response is the account_id — use it to link products back to accounts",
        "display_upc may differ from the canonical UPC stored in the system"
      ],
      "relationships": [
        "Product -> Account/Vendor (N:1, via vendor_id = account_id)",
        "Product -> Subaccount (N:1, optional grouping)",
        "Product -> Tracks (1:N, identified by ISRC)"
      ]
    },
    {
      "terms": ["UPC", "barcode", "EAN"],
      "targets": ["get_product_by_upc"],
      "related": ["get_product"],
      "domain": "product",
      "priority": 2,
      "context": "Looks up a product (release) by its UPC/EAN barcode (12-13 digit number, e.g. 888003244085). Returns product details including name, release date, and status. Use when you have a barcode but not the product_id. Display UPCs as-is in responses.",
      "gotchas": [
        "Never ask the user to reformat an identifier — accept any format and pass as-is",
        "UPC → get_product_by_upc (returns vendor_id) → account_overview_skill (vendor_id = account_id) — includes account details and associated contracts"
      ]
    },
    {
      "terms": ["ISRC", "track code", "recording code"],
      "targets": ["get_products_by_isrc", "find_contract_terms_by_attachment"],
      "domain": "product",
      "priority": 2,
      "context": "Use get_products_by_isrc to find which products contain a track with a given ISRC. Use find_contract_terms_by_attachment to find which contract covers a specific ISRC or UPC (requires account_id, attachment, and term_type). ISRC format: CC-XXX-YY-NNNNN (e.g. BR-OQA-13-00182). Accept any input format (dashes, spaces, or none) — pass to the tool as-is. Always display in dashed format in responses.",
      "gotchas": [
        "find_contract_terms_by_attachment can search by ISRC or UPC and requires specifying the term_type",
        "Never ask the user to reformat an identifier — accept any format and pass as-is",
        "ISRC → get_products_by_isrc (returns products with vendor_id) → account_overview_skill (vendor_id = account_id); if multiple products returned, present all with names, UPCs, and vendor IDs — do not silently pick the first"
      ]
    },
    {
      "terms": ["SQL", "database", "warehouse", "data warehouse", "query data"],
      "targets": [
        "snowflake_explore_skill",
        "query_snowflake",
        "search_snowflake_schema"
      ],
      "related": ["describe_snowflake_table"],
      "domain": "snowflake",
      "priority": 2,
      "context": "Use snowflake_explore_skill as the primary entry point for data warehouse queries: it performs hybrid search, shows columns and FK relationships, and suggests a SELECT query. Workflow: snowflake_explore_skill → query_snowflake with the suggestedQuery (2 rounds). Skip describe_snowflake_table — search results already include full column schemas; only call it if you already know a table by name. Use relatedTables from search results to discover join paths (inferred from _ID column naming conventions, e.g. VENDOR_ID → DIM_VENDOR).",
      "gotchas": [
        "query_snowflake only allows SELECT and WITH statements; no DML or DDL",
        "Results are capped at 1000 rows (MAX_ROW_LIMIT); default is 500 if no LIMIT specified",
        "LIMIT is auto-appended if missing; existing LIMIT is enforced to the cap",
        "No SQL comments allowed; single statement only; must use fully-qualified table names (DATABASE.SCHEMA.TABLE)",
        "Always include a LIMIT clause in SQL queries"
      ],
      "businessRules": [
        "Identity-scoped connection pool provides row-level security; queries run as the authenticated user",
        "Table identifiers are sanitized (quotes, semicolons removed) and LIKE patterns are escaped to prevent injection",
        "Golden search term mappings: 'revenue' → VW_REVENUE_BY_COUNTRY, 'contract' → VW_OA_CONTRACT, 'balance' → VW_ABACUS_BALANCES_LOOKER_V2, 'vendor' → VW_DIM_ABACUS_AR_VENDOR, 'flowthrough' → VW_ABACUS_AUTOMATED_FLOWTHROUGH"
      ]
    },
    {
      "terms": ["table", "tables", "columns", "schema"],
      "targets": ["snowflake_explore_skill", "search_snowflake_schema"],
      "related": ["describe_snowflake_table"],
      "domain": "snowflake",
      "priority": 1,
      "context": "Use snowflake_explore_skill or search_snowflake_schema to discover available tables and their columns via hybrid search. Use describe_snowflake_table for detailed column schema of a specific table."
    },
    {
      "terms": ["API", "graphql", "gateway", "federation"],
      "targets": ["graphql_explore_skill", "query_graphql"],
      "related": ["search_graphql", "graphql_type_info"],
      "domain": "graphql",
      "priority": 2,
      "context": "Use graphql_explore_skill as the primary entry point: it performs hybrid search with auto-resolved types and can optionally auto-execute queries. Preferred (1 round): call with auto_execute=true and variables when you already have argument values — returns schema info AND query results in one call. Fallback (2 rounds): call without auto_execute, then query_graphql with the returned suggestedQuery. search_graphql and graphql_type_info are rarely needed — only if the explore skill didn't surface the field or type you need.",
      "businessRules": [
        "Only query operations are allowed; mutations and subscriptions are blocked",
        "Introspection queries (__schema, __type) are blocked for security",
        "Golden search term mappings: 'royalties' → abacusContract/abacusContracts, 'distribution' → delivery/deliveryStores, 'product' → product/products, 'user' → identityByEmail/identityById"
      ],
      "gotchas": [
        "On HTTP 400 / schema validation errors, do NOT retry with a different query — the error is structural; fall back to a REST skill or tell the user the data isn't available via GraphQL",
        "Partial null fields are normal in a federated schema (access-restricted or not applicable) — present whatever non-null data was returned; do not treat partial nulls as failure",
        "On completely empty results (no data at all, not an error), try one alternative search term with the explore skill before giving up",
        "Schema loading falls back to on-demand fetch if startup loading failed; first query may be slower"
      ]
    },
    {
      "terms": ["excel", "spreadsheet", "xlsx", "csv", "export"],
      "targets": ["generate_excel"],
      "related": ["generate_pdf"],
      "domain": "file",
      "priority": 2,
      "context": "Generates a downloadable .xlsx file from tabular data. Supports multi-sheet workbooks. Use when the user wants data exported as a spreadsheet. Always fetch the data first using appropriate data tools, then pass results to generate_excel — never call it without having data.",
      "gotchas": [
        "Use descriptive filenames in snake_case incorporating the entity and data type (e.g. 'account_12345_revenue_by_store_2024')",
        "Organise data logically: use multiple sheets when presenting different data categories; include a meaningful title; format numbers as numbers, not strings",
        "Always include a text response alongside the file — tell the user what the file contains and summarise the key data points; never respond with only the file attachment"
      ]
    },
    {
      "terms": ["pdf", "document", "report"],
      "targets": ["generate_pdf"],
      "related": ["generate_excel"],
      "domain": "file",
      "priority": 2,
      "context": "Generates a downloadable PDF from structured content including headings, text blocks, and tables. Use when the user wants a formatted document or report. Always fetch the data first using appropriate data tools, then pass results to generate_pdf — never call it without having data.",
      "gotchas": [
        "Use descriptive filenames in snake_case incorporating the entity and data type (e.g. 'contract_678_advance_history')",
        "Write a clear title, use section headings, include explanatory text before tables, and add a summary or key takeaways section — make the report readable as a standalone document",
        "Always include a text response alongside the file — tell the user what the file contains and summarise the key data points; never respond with only the file attachment"
      ]
    },
    {
      "terms": ["overview", "summary", "tell me about", "what do you know"],
      "targets": [
        "account_overview_skill",
        "contract_overview_skill",
        "revenue_overview_skill"
      ],
      "domain": "skill",
      "priority": 1,
      "context": "High-level overview skills that aggregate multiple data points in a single call. Use these when the question names a specific entity type (account, contract, revenue) — prefer the matching REST skill over fanning out to explore skills. account_overview_skill covers account details, payments, tax, and contracts. contract_overview_skill covers terms, advances, parties, lifecycle, and run status. revenue_overview_skill covers periods, store breakdown, and artist breakdown.",
      "gotchas": [
        "Overview skills are the most efficient starting point; prefer them over calling individual tools unless you need a specific slice of data",
        "Skills use resilientCall wrapper — individual sub-calls can fail without failing the entire skill; check the errors array in results"
      ],
      "businessRules": [
        "account_overview_skill aggregates 6 parallel calls: details, payment term, eligibility, tax info, activity, contracts",
        "contract_overview_skill aggregates 6 parallel calls: contract, terms, advances (all statuses), parties, lifecycle, run_controller",
        "revenue_overview_skill aggregates 4 parallel calls: statement_periods, activity, revenue_by_store, revenue_by_artist"
      ]
    },
    {
      "terms": ["run status", "processing", "accounting run", "run controller"],
      "targets": [
        "get_run_controller_by_contract",
        "get_run_controller_contracts_by_account"
      ],
      "domain": "royalties",
      "priority": 2,
      "context": "Shows the accounting run status for contracts. Use get_run_controller_by_contract with a contract_id for a single contract. Use get_run_controller_contracts_by_account with an account_id to see run status across all contracts on an account.",
      "businessRules": [
        "Accounting runs must complete before royalties for a period are finalized and payable",
        "Run Controller groups sibling contracts (same account + contract type) for batch processing",
        "When a contract has isExcludedFromAccountingRun, it is skipped during runs but retains its status",
        "Terminated contracts are excluded from run controller updates; when a new contract is a run controller's first, an AccountingRun is auto-created in any open period"
      ],
      "gotchas": [
        "Contract lifecycle transitions (e.g., TO_BE_TERMINATED → TERMINATED) are triggered automatically by the Run Controller during accounting runs, not by users",
        "When an accounting run transitions to INVALID, a clone run is automatically created for the same controller — the original stays as historical record"
      ],
      "relationships": [
        "RunController -> Contract (1:N, groups sibling contracts)",
        "RunController -> AccountingRun (1:N, one run per accounting period)"
      ]
    },
    {
      "terms": ["exclusion", "territory exclusion", "excluded", "territory"],
      "targets": ["get_contract_exclusions"],
      "related": ["search_contracts"],
      "domain": "royalties",
      "priority": 2,
      "context": "Returns territory and product exclusions carved out of contract terms. Requires contract_id. Shows which territories or products are explicitly excluded from royalty calculations under a contract.",
      "businessRules": [
        "Exclusions override contract terms for specific territories or products; revenue from excluded items is not royalty-bearing"
      ],
      "relationships": [
        "ContractExclusion -> Contract (N:1, each exclusion belongs to one contract)",
        "ContractExclusion -> Territory or Product (scope of the carve-out)"
      ]
    },
    {
      "terms": ["explore", "discover", "find data", "look up"],
      "targets": ["graphql_explore_skill", "snowflake_explore_skill"],
      "related": ["search_graphql", "search_snowflake_schema"],
      "domain": "skill",
      "priority": 1,
      "context": "Entry points for data discovery. Source selection order: (1) if the question names a specific entity type (account, contract, product, balance), use the matching REST skill — don't fan out; (2) if the question is analytical, comparative, or catalog-wide (trends, rankings, stream counts), use snowflake_explore_skill directly — no account ID needed for platform-wide questions; (3) if the question spans teams or systems (publishing rights, ownership, DSP delivery), use graphql_explore_skill; (4) if the domain is unclear, fan out to both explore skills in the same turn. Default to Snowflake for analytical/aggregate questions and GraphQL for cross-team/operational data.",
      "gotchas": [
        "When domain is unclear, fan out in the FIRST turn — do not probe one source then fall back to the other",
        "Fan out when: domain unclear from context, or user asks for a full picture across systems. Do NOT fan out when: domain already established, user named a specific system, or a prior search confirmed only one source is relevant",
        "Search phase: call both explore skills in the same turn (graphql_explore_skill with auto_execute=false, snowflake_explore_skill). If neither is available yet, call search_tools first to discover them",
        "Execution phase: if both returned relevant non-overlapping data, run both queries in the same turn; if one source clearly has everything needed, skip the other",
        "Fallback: if one source returns nothing useful, present the other without retrying; if it errored or returned access-denied, note it briefly; if it simply returned no results, omit silently",
        "Presentation: use '### From GraphQL' and '### From Snowflake' subheadings when both sources contribute; never silently mix totals from different sources; lead with a shared intro when results are complementary"
      ]
    },
    {
      "terms": [
        "analytics",
        "ad-hoc",
        "ad hoc",
        "custom query",
        "data analysis"
      ],
      "targets": ["snowflake_explore_skill", "query_snowflake"],
      "related": ["search_snowflake_schema"],
      "domain": "snowflake",
      "priority": 1,
      "context": "For ad-hoc analytics and custom data analysis. Always use Snowflake for analytical, comparative, or catalog-wide questions (trends, rankings, stream counts, platform-wide queries like 'top streamed songs'). These do not need an account ID. Start with snowflake_explore_skill to discover relevant tables and get a suggested query, then execute with query_snowflake. Results are capped at 1000 rows.",
      "gotchas": [
        "Always explore the schema first before writing queries to ensure correct table and column names"
      ]
    },
    {
      "terms": ["publishing", "ownership", "label", "label copy"],
      "targets": ["graphql_explore_skill"],
      "related": ["search_graphql", "query_graphql"],
      "domain": "graphql",
      "priority": 1,
      "context": "Publishing and ownership data lives in the GraphQL federated gateway. Default to GraphQL when the question involves data that spans multiple services or teams (publishing rights, ownership splits, label relationships, DSP delivery, identity). Use graphql_explore_skill to discover available types and fields, then query_graphql to execute queries for label copy, publishing shares, or ownership details."
    },
    {
      "terms": [
        "run code",
        "execute",
        "sandbox",
        "script",
        "compute",
        "calculate",
        "transform data"
      ],
      "targets": ["execute_code"],
      "related": [],
      "domain": "runner",
      "priority": 2,
      "context": "Runs JavaScript or TypeScript in a sandboxed V8 isolate with a 30-second wall time and 128MB heap limit. Has a data.fetch() bridge to call other tools from within the sandbox. Use for custom computations, data transformations, or complex calculations.",
      "gotchas": [
        "30-second wall time limit and 128MB heap; keep computations focused",
        "Use data.fetch() bridge inside the sandbox to call other tools, not direct HTTP",
        "Results are capped at 1MB; if output exceeds this, the execution fails",
        "If runner service is unavailable, all code execution tools are disabled"
      ],
      "businessRules": [
        "data.fetch() bridge only allows methods in the user's allowedTools permission list; unauthorized methods return permission denied"
      ]
    },
    {
      "terms": [
        "datasource",
        "data source",
        "refresh",
        "reload",
        "sync",
        "update data"
      ],
      "targets": ["refresh_datasource", "get_refresh_status"],
      "related": ["execute_code"],
      "domain": "runner",
      "priority": 2,
      "context": "Use refresh_datasource to trigger an async datasource refresh (returns immediately). Use get_refresh_status to check whether the refresh has completed. Refresh is a destructive operation that replaces existing data.",
      "gotchas": [
        "refresh_datasource is destructive and replaces existing data; confirm with the user before triggering",
        "Refresh is async; always follow up with get_refresh_status to check completion"
      ]
    },
    {
      "terms": [
        "neighbouring rights",
        "neighboring rights",
        "NR",
        "performance rights",
        "broadcast royalties"
      ],
      "targets": ["search_contracts", "contract_overview_skill"],
      "related": ["get_contract_exclusions", "get_run_controller_by_contract"],
      "domain": "royalties",
      "priority": 1,
      "context": "Neighbouring rights (NR) are a separate contract type from distribution. NR contracts use different calculation rules, have separate accounting run results tables (ACCOUNTING_RUN_RESULTS_NR), and track NR-specific contributors. Use search_contracts with contractType filter or check contractType field in results to distinguish NR from distribution contracts.",
      "gotchas": [
        "NR contracts have completely different calculation and payment rules from distribution contracts — do not confuse them",
        "NR accounting run results include royalty_rate from contract_term_condition, withholding_tax columns, and sale/payee currency conversions",
        "NR may track delivery status and claim processing separately from distribution"
      ],
      "businessRules": [
        "contractType 'NEIGHBOURING_RIGHTS' is a parallel system to 'DISTRIBUTION' with separate processing",
        "NR contract terms have different condition types and rate structures"
      ]
    },
    {
      "terms": [
        "flowthrough",
        "flow through",
        "flow-through",
        "distribution split",
        "sub-distribution"
      ],
      "targets": ["contract_overview_skill", "search_contracts"],
      "related": ["get_contract_exclusions", "get_historical_advances"],
      "domain": "royalties",
      "priority": 1,
      "context": "Flowthrough is a contract term type (FLOWTHROUGH) that distributes earnings from one account to sub-recipients. Flowthrough status is independent of contract status: a contract can be ACTIVE while its flowthrough is PAUSED or SHUTOFF. Use contract_overview_skill to check flowthrough status on a contract.",
      "gotchas": [
        "Flowthrough status (ACTIVE | PAUSED | SHUTOFF) is independent of the parent contract status",
        "Adjustments with type 'Flowthrough' require an explicit apply_to_flowthrough_payment flag (Y/N)"
      ],
      "businessRules": [
        "FLOWTHROUGH is one of several contract term types: DISTRO, FLOWTHROUGH, MECHANICAL_DEDUCTION, NR",
        "PAUSED temporarily stops flowthrough distribution; SHUTOFF permanently disables it"
      ]
    },
    {
      "terms": ["notion", "notion page", "notion database", "wiki"],
      "targets": ["search_notion", "read_notion_page", "query_notion_database"],
      "domain": "notion",
      "priority": 2,
      "context": "Notion tools are loaded on demand via search_tools. If they appear, the user has connected their workspace. Use search_notion to find pages/databases by title, read_notion_page for full markdown content, query_notion_database for filtered queries. If search_tools returns no Notion tools, the user hasn't connected — suggest Settings > Integrations.",
      "gotchas": [
        "Notion tools only appear after the user connects their workspace via OAuth — always discover via search_tools first",
        "search_notion finds by title; read_notion_page returns full markdown; query_notion_database supports filters and sorts"
      ]
    }
  ]
}
