{
  "_comment": "CRM widget subtypes. Source of truth for the zoho-widgets-crm-* skills; each renders as its own skill = this entry + core.md. Types are the eight Zoho documents at https://www.zoho.com/crm/developer/docs/widgets/. SDK surface and events were scraped from https://help.zwidgets.com/help/latest/ (JS SDK v1.2).",
  "subtypes": [
    {
      "slug": "button",
      "name": "Custom Button",
      "title": "Widget in a Custom Button",
      "where": "Opens when a user clicks a custom button you place on a record's detail page, on a list view, or on a related list.",
      "trigger": "The button's click. CRM opens the widget in a popup, a sliding panel or a new tab, chosen when you create the button.",
      "context": "`PageLoad` gives you `Entity`, `EntityId` and `ButtonPosition`. On a **list view** `EntityId` is an ARRAY of the selected records; on a detail page it is a single id. Zoho's own example:\n\n```json\n{ \"EntityId\": [\"3000000040011\", \"3000000032101\"], \"Entity\": \"Leads\", \"ButtonPosition\": \"ListView\" }\n```",
      "apis": ["ZOHO.CRM.API.*", "ZOHO.CRM.UI.Popup.close / closeReload", "ZOHO.CRM.UI.Record.open / edit / populate", "ZOHO.CRM.FUNCTIONS.execute", "ZOHO.CRM.CONNECTION.invoke"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", async function (data) {\n  // A list-view button hands you many ids; a detail-page button hands you one.\n  const ids = Array.isArray(data.EntityId) ? data.EntityId : [data.EntityId];\n  const module = data.Entity;\n\n  for (const id of ids) {\n    const r = await ZOHO.CRM.API.updateRecord({\n      Entity: module,\n      APIData: { id: id, Lead_Status: \"Contacted\" }\n    });\n    if (r.data[0].code !== \"SUCCESS\") { console.error(id, r.data[0]); }\n  }\n\n  // Close the popup and refresh the underlying page so the user sees the change.\n  ZOHO.CRM.UI.Popup.closeReload();\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "**Always handle `EntityId` being an array.** The same widget is reachable from a list view, where the user may have selected 200 records; treating it as a string is the most common bug in button widgets.",
        "Finish by calling `ZOHO.CRM.UI.Popup.closeReload()` when you changed data, or `close()` when you did not — otherwise the user is left staring at your popup and the record behind it looks stale.",
        "A list-view button can be clicked on a large selection. Batch, show progress, and do not fire hundreds of parallel writes.",
        "The button's own configuration (where it appears, popup vs tab, which profiles see it) is set in CRM Setup, not in the widget."
      ]
    },
    {
      "slug": "relatedlist",
      "name": "Custom Related List",
      "title": "Widget in a Custom Related List",
      "where": "A section in the Related List area of a record's detail page, alongside Notes, Attachments and the standard related lists.",
      "trigger": "Loads with the record's detail page. It is visible whenever that record is open, so it must be fast and must not act on its own.",
      "context": "`PageLoad` gives the parent record. Zoho's own example:\n\n```json\n{ \"Entity\": \"Leads\", \"EntityId\": \"3000000032096\" }\n```",
      "apis": ["ZOHO.CRM.API.getRecord / getRelatedRecords / searchRecord / coql", "ZOHO.CRM.UI.Resize", "ZOHO.CRM.UI.Record.open", "ZOHO.CRM.CONNECTION.invoke", "ZOHO.CRM.FUNCTIONS.execute"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", async function (data) {\n  const { Entity, EntityId } = data;\n\n  // Pull whatever this list is meant to show — related records, or data from\n  // your own system keyed on a field of the parent record.\n  const rel = await ZOHO.CRM.API.getRelatedRecords({\n    Entity: Entity, RecordID: EntityId, RelatedList: \"Notes\", page: 1, per_page: 20\n  });\n  render(rel.data || []);\n\n  // Grow the frame to fit what you rendered — the panel does not auto-size.\n  ZOHO.CRM.UI.Resize({ height: String(document.body.scrollHeight) });\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "**It loads on every view of the record**, so keep it to one or two calls and cache nothing sensitive. A slow related-list widget makes the whole detail page feel slow.",
        "Call `ZOHO.CRM.UI.Resize` after rendering. The frame has a fixed default height and your content will otherwise be clipped or leave a gap.",
        "This is a read-and-display surface. Put actions behind an explicit click, not on load — the user did not ask you to write anything by opening a record.",
        "`ZOHO.CRM.UI.Record.open({ Entity, RecordID })` is how you navigate the user to a related record instead of building your own links."
      ]
    },
    {
      "slug": "webtab",
      "name": "Web Tab",
      "title": "Widget in a Web Tab",
      "where": "A full-page tab in the CRM top navigation, next to Leads, Contacts and Deals.",
      "trigger": "The user clicking the tab. There is no record context — the widget owns the whole page.",
      "context": "**No `PageLoad` record context.** A web tab is not tied to an entity, so `Entity`/`EntityId` are not provided. Establish your own context with `ZOHO.CRM.CONFIG.getCurrentUser()` and `getOrgInfo()`, then let the user choose what to work on.",
      "apis": ["ZOHO.CRM.CONFIG.getCurrentUser / getOrgInfo", "ZOHO.CRM.API.* (coql, searchRecord, getAllRecords)", "ZOHO.CRM.META.getModules / getFields", "ZOHO.CRM.UI.Record.open / create", "ZOHO.CRM.CONNECTION.invoke"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", function () {\n  // A web tab gets no record — start from the user and the org.\n});\nZOHO.embeddedApp.init();\n\nasync function boot() {\n  const me  = await ZOHO.CRM.CONFIG.getCurrentUser();\n  const org = await ZOHO.CRM.CONFIG.getOrgInfo();\n\n  // COQL is the efficient way to build a dashboard-style view.\n  const rows = await ZOHO.CRM.API.coql({\n    select_query: \"select id, Deal_Name, Amount, Stage from Deals where Stage = 'Closed Won' limit 200\"\n  });\n  render(me.users[0], org, rows.data || []);\n}\nboot();",
      "rules": [
        "This is a whole application page, so it owns its own routing, empty states and error handling. Nothing else on screen belongs to you.",
        "There is no record context — never assume `data.EntityId`. Ask the user, or derive from the current user.",
        "It is the one widget type where users will linger, so paginate and use `coql` rather than pulling whole modules.",
        "The tab (name, icon, which profiles see it) is configured in CRM Setup; the widget only supplies the page."
      ]
    },
    {
      "slug": "dashboard",
      "name": "Dashboard",
      "title": "Widget in a Dashboard",
      "where": "A component inside a CRM Dashboard, sitting among the standard charts and KPIs.",
      "trigger": "Loads when the dashboard is opened or refreshed. Sized by the dashboard grid cell, not by you.",
      "context": "No record context. The widget renders a visualisation of data it fetches itself, for the current user.",
      "apis": ["ZOHO.CRM.API.coql / getAllRecords / searchRecord", "ZOHO.CRM.CONFIG.getCurrentUser", "ZOHO.CRM.META.getModules", "ZOHO.CRM.CONNECTION.invoke", "ZOHO.CRM.FUNCTIONS.execute"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", async function () {\n  // Aggregate in the query, not in the browser: one COQL beats paging a module.\n  const r = await ZOHO.CRM.API.coql({\n    select_query: \"select Stage, COUNT(id) from Deals where id is not null group by Stage\"\n  });\n  drawChart(r.data || []);\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "A dashboard loads several components at once. Make **one** aggregate query — `coql` with `COUNT`/`SUM` and `group by` — rather than fetching rows and counting in JavaScript.",
        "The cell is small and fixed. Design for it; do not call `Resize` to fight the dashboard grid.",
        "Users refresh dashboards often, so the widget must be idempotent and cheap. Never write data from a dashboard widget.",
        "COQL rules apply: a `WHERE` clause is mandatory, `limit` caps at 2000, aggregates need `group by`."
      ]
    },
    {
      "slug": "blueprint",
      "name": "Blueprint",
      "title": "Widget in a Blueprint",
      "where": "Inside a Blueprint transition — the panel a user fills in to move a record from one state to the next.",
      "trigger": "The user taking that transition. Your widget gathers or validates what the transition needs, then completes it.",
      "context": "`PageLoad` provides the record under the blueprint (`Entity`, `EntityId`). The transition itself is driven through `ZOHO.CRM.BLUEPRINT.proceed`.",
      "apis": ["ZOHO.CRM.BLUEPRINT.proceed", "ZOHO.CRM.API.getBluePrint / updateBluePrint", "ZOHO.CRM.API.getRecord / updateRecord", "ZOHO.CRM.CONNECTION.invoke", "ZOHO.CRM.FUNCTIONS.execute"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", async function (data) {\n  const { Entity, EntityId } = data;\n\n  // What the blueprint currently allows for this record.\n  const bp = await ZOHO.CRM.API.getBluePrint({ Entity: Entity, RecordID: EntityId });\n  render(bp);\n\n  document.querySelector(\"#done\").addEventListener(\"click\", async function () {\n    // Hand control back to CRM to complete the transition.\n    await ZOHO.CRM.BLUEPRINT.proceed();\n  });\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "**Finish with `ZOHO.CRM.BLUEPRINT.proceed()`.** Without it the transition never completes and the record stays in its old state, however much you wrote.",
        "A record governed by a blueprint **rejects a direct update to the governed field** — move the state through the transition, not with `updateRecord`.",
        "Validate before proceeding: once the transition completes, the record has moved on and reversing it is a separate transition, if one exists at all.",
        "Keep it single-purpose. A blueprint panel is a step in someone's process, not a place for a general-purpose UI."
      ]
    },
    {
      "slug": "wizard",
      "name": "Wizard",
      "title": "Widget in a Wizard",
      "where": "A screen inside a CRM Wizard — the multi-step guided form used for structured data entry.",
      "trigger": "The user reaching that step of the wizard. The widget is one screen among several.",
      "context": "`PageLoad` gives the wizard's record context. Values are handed back to the wizard with `ZOHO.CRM.WIZARD.post`, not written directly.",
      "apis": ["ZOHO.CRM.WIZARD.post", "ZOHO.CRM.API.getRecord / searchRecord", "ZOHO.CRM.META.getFields", "ZOHO.CRM.CONNECTION.invoke", "ZOHO.CRM.FUNCTIONS.execute"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", function (data) {\n  document.querySelector(\"#next\").addEventListener(\"click\", async function () {\n    // Give the collected values back to the wizard; it owns the record write.\n    await ZOHO.CRM.WIZARD.post({\n      // field API names, exactly as configured on the wizard's layout\n      Company: document.querySelector(\"#company\").value,\n      Annual_Revenue: Number(document.querySelector(\"#revenue\").value)\n    });\n  });\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "**The wizard owns the record.** Return values with `ZOHO.CRM.WIZARD.post` instead of calling `updateRecord` yourself, or the wizard's own validation and later steps work from stale values.",
        "Keys must be field **API names** from the wizard's layout — check with `ZOHO.CRM.META.getFields`; a wrong name is silently dropped.",
        "The user can go back. Your screen must render correctly when revisited with values already set.",
        "Do not navigate the user away mid-wizard; you are one screen inside someone else's flow."
      ]
    },
    {
      "slug": "signal",
      "name": "Signal",
      "title": "Widget in a Signal",
      "where": "Behind a SalesSignal notification — the alert bell where CRM surfaces activity from email, calls, chat and integrations.",
      "trigger": "The user opening the signal. The widget shows the detail behind the notification.",
      "context": "The signal's own payload, delivered when the widget opens. Signals are raised by your extension or by Deluge, not by the widget itself.",
      "apis": ["ZOHO.CRM.API.getRecord / addNotes", "ZOHO.CRM.UI.Record.open", "ZOHO.CRM.CONNECTION.invoke", "ZOHO.CRM.FUNCTIONS.execute"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", async function (data) {\n  // Render the detail behind the notification, then let the user act on it.\n  render(data);\n\n  document.querySelector(\"#open-record\").addEventListener(\"click\", function () {\n    ZOHO.CRM.UI.Record.open({ Entity: data.Entity, RecordID: data.EntityId });\n  });\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "The widget is the **view**, not the source. Signals are raised elsewhere (Deluge, your extension's backend); this only renders and lets the user respond.",
        "The user is mid-task and glanced at a notification, so make the point in one screen and offer one obvious action.",
        "`ZOHO.CRM.UI.Record.open` hands them to the real record rather than rebuilding it inside the popup.",
        "Do not write on open — a signal being read is not consent to change anything."
      ]
    },
    {
      "slug": "settings",
      "name": "Settings",
      "title": "Widget in Settings",
      "where": "A configuration page for your extension, under CRM Setup.",
      "trigger": "An administrator opening your extension's settings. Runs with admin context, usually once per org.",
      "context": "No record context. This is where an org's configuration for your extension is collected and stored.",
      "apis": ["ZOHO.CRM.API.getOrgVariable", "ZOHO.CRM.CONFIG.getCurrentUser / getOrgInfo", "ZOHO.CRM.CONNECTION.invoke", "ZOHO.CRM.CONNECTOR.authorize", "ZOHO.CRM.FUNCTIONS.execute", "ZOHO.CRM.META.getModules / getFields"],
      "sample": "ZOHO.embeddedApp.on(\"PageLoad\", async function () {\n  const me = await ZOHO.CRM.CONFIG.getCurrentUser();\n\n  // Read existing configuration from an org variable — never from localStorage,\n  // which is per browser and would give each admin a different answer.\n  const current = await ZOHO.CRM.API.getOrgVariable(\"my_ext_default_owner\");\n  render(me.users[0], current);\n\n  document.querySelector(\"#save\").addEventListener(\"click\", async function () {\n    // Org variables are written server-side: a Deluge function, not the widget.\n    await ZOHO.CRM.FUNCTIONS.execute(\"my_ext_save_settings\", {\n      arguments: JSON.stringify({ default_owner: document.querySelector(\"#owner\").value })\n    });\n  });\n});\nZOHO.embeddedApp.init();",
      "rules": [
        "**Store configuration in org variables**, written through a Deluge function. `localStorage` is per browser and per user, so settings would silently differ between admins and vanish on a new machine.",
        "**Never collect a third-party API key into your own storage.** Use a Connection (`ZOHO.CRM.CONNECTOR.authorize`) so Zoho holds the credential and the widget never sees it.",
        "Assume an administrator, but check: `getCurrentUser()` tells you the profile, and a non-admin should get a clear message rather than a broken form.",
        "This page usually runs once, at install. Make the empty, first-run state the one you design for."
      ]
    }
  ]
}
