{"slug": "using-the-kanban-board-component-in-ui-builder", "title": "Using the Kanban Board Component in UI Builder", "summary": "Kunal Khatri documented a step-by-step guide for building a Kanban board for incidents in ServiceNow's UI Builder (Next Experience), using the Kanban Board component from the sn-vtb library. The board groups incidents by priority, with lanes and cards driven by data resources and client scripts, including a GraphQL mutation for updating records on card moves. The configuration includes Transform and Composite data resources, client state parameters, and three client scripts to reproduce the board in any instance.", "body_md": "Disclaimer:This article was written by an LLM. All the underlying content — configuration steps, scripts, and reference material — was carefully collected and verified by Kunal Khatri.\n\nThis article is a companion write-up to our video walkthrough on building a Kanban board for Incidents in UI Builder (Next Experience). It documents, end to end, the configuration steps we performed on screen and includes the exact client scripts we used, so you can reproduce the same board - lanes, cards, styling, lane footers, and the \"Add Card\" button - in your own instance.\n\nIt is meant as an addendum to two excellent ServiceNow Community articles on the same component, and two \"You & I Builder Live\" sessions that cover the component interactively. Links to all four are in [Further Reading](#further-reading) at the end.\n\nThe board in this article is bound to the `incident`\n\ntable and groups incidents into lanes by **Priority**. Everything is driven by three UI Builder building blocks:\n\n| Element | Type | Purpose |\n|---|---|---|\n`get_priority_values_1` |\nTransform data resource | Fetches the Priority choice list for `incident` , used to generate one lane per priority value |\n`look_up_multiple_records_1` |\nComposite (\"Look up multiple records\") data resource | Fetches active incidents (`active=true` , up to 1000 rows) with the fields needed for the cards |\n`update_card` |\nGraphQL data resource (explicit/manual execution) | Writes `impact` /`urgency` /`work_notes` back to the incident when a card is dragged to a new lane |\n`lanesData` |\nClient State Parameter (JSON) | Backing array for the board's `lanes` property |\n`cardsData` |\nClient State Parameter (JSON) | Backing array for the board's `cards` property |\n`newIncidentSysID` |\nClient State Parameter (string) | Feeds the sys_id of a newly created incident into a record-creation modal |\n`kanban_board_1` |\nKanban Board (`sn-vtb` ) macroponent |\nThe board itself, with `lanes` and `cards` bound to the two state parameters above |\n\nTwo client scripts run automatically when their data resource fetches succeed (`SetLanes`\n\n, `SetCards`\n\n), and one runs in response to the board's `CARD_MOVED`\n\nevent (`Move cards`\n\n). All three are reproduced in full below, at the step where they're relevant.\n\n- Open the page in UI Builder and drag the\n**Kanban Board** component from the component picker onto the canvas. - Before you can bind any data to it, create two\n**Client State Parameters** of type`json`\n\n:`lanesData`\n\n- initial value`null`\n\n`cardsData`\n\n- initial value`null`\n\n- Select the Kanban Board component and bind its properties:\n`lanes`\n\n→**State Binding**→`lanesData`\n\n`cards`\n\n→**State Binding**→`cardsData`\n\nAt this point the board renders but is empty - nothing has populated `lanesData`\n\nor `cardsData`\n\nyet. That's what the next two steps do.\n\nThe Kanban Board component ships as part of the\n\n`sn-vtb`\n\n(Visual Task Board) component library - make sure it's installed/enabled on your instance before it shows up in the component picker, as covered in the[Part 1 reference article].\n\nLanes come from data, not from manual configuration in the UI Builder canvas. We added a **Transform** data resource, `get_priority_values_1`\n\n, that returns the Priority choice list for the `incident`\n\ntable (an array of `{ sys_id, label, value }`\n\nobjects).\n\nA Transform data resource is backed by a server-side script (the \"transform data broker\"), which runs on the server and returns whatever the client-side data resource then exposes as `.output`\n\n. Ours queries `sys_choice`\n\nfor the `task`\n\ntable's `priority`\n\nelement, ordered by value:\n\n``` js\nfunction transform(){\n    let choice_list = [];\n    var gr = new GlideRecord(\"sys_choice\");\n    gr.addEncodedQuery(\"language=en^name=task^elementSTARTSWITHpriority\");\n    gr.orderBy(\"value\");\n    gr.query();\n    while(gr.next()){\n        choice_list.push({\n            \"sys_id\":gr.getUniqueValue(),\n            \"label\":gr.getValue(\"label\"),\n            \"value\":gr.getValue(\"value\")\n        });\n    }\n    return choice_list;\n}\n```\n\nA couple of notes on this query:\n\n- We query\n`sys_choice`\n\non`name=task`\n\n(not`name=incident`\n\n) because`priority`\n\nis defined on the base`task`\n\ntable and inherited by`incident`\n\n- querying`task`\n\npicks up the choice list regardless of which task-derived table you point the board at. `elementSTARTSWITHpriority`\n\nmatches the`priority`\n\nelement; using`STARTSWITH`\n\nrather than an exact match is a defensive habit here, though an exact`element=priority`\n\nwould work identically for this field.`orderBy(\"value\")`\n\ngives us the choices in priority order (1–Critical through 5–Planning), which becomes the left-to-right lane order on the board since`SetLanes`\n\nbelow just maps the array in place.\n\nThis script only produces the raw choice list - it doesn't know anything about lanes, styling, or the board. That transformation happens client-side, in the script mapped to the data resource's `DATA_FETCH_SUCCEEDED`\n\nevent.\n\nWe mapped a client script to that event. This script (`SetLanes`\n\n) turns each priority choice into a lane object and writes the result to `lanesData`\n\n:\n\n```\n/**\n* @param {params} params\n* @param {api} params.api\n* @param {any} params.event\n* @param {any} params.imports\n* @param {ApiHelpers} params.helpers\n*/\nfunction handler({api, event, helpers, imports}) {\n    var data = api.data.get_priority_values_1.output;\n    var return_value = data.map(choice=>{\n        return {\n            \"id\": \"lane_\"+choice.value,\n            \"title\": choice.label,\n            \"name\": choice.label,\n            \"_style_\": {\n                \"sn-lane\": {\n                    \"width\": \"285px\",\n                    \"padding\": \"3px\",\n                    \"overflow-y\": \"auto\",\n                    \"height\": \"calc(100% - 58px)\"\n                },\n                \"lane-footer-container\": {\n                    \"position\": \"sticky\",\n                    \"bottom\": \"30px\"\n                }\n            },\n            \"options\": {\n                \"board_type\": \"FREEFORM\",\n                \"is2DBoard\": false,\n                \"lane_filter\": \"\",\n                \"lane_count\": 3,\n                \"total_lane_count\": 3,\n                \"lane_field\": \"__KANBAN__\",\n                \"swimlane_field\": \"\",\n                \"table\": \"incident\",\n                \"highest_lane_order\": 2,\n                \"can_add_task\": true,\n                \"add_card_footer_button_name\": \"New Incident\"\n            }\n        }\n    });\n    api.setState(\"lanesData\",return_value);\n\n}\n```\n\nA couple of things worth calling out:\n\n- every lane's`id: \"lane_\" + choice.value`\n\n`id`\n\nis derived from the priority value (e.g.`lane_1`\n\n,`lane_2`\n\n, …). We use this exact prefix again in the card script, so cards land in the correct lane. This`lane_id`\n\ncontract between the two scripts is the single most important detail to get right.and`_style_`\n\nare set per lane here - see Steps 4–6 below for what each of those keys does.`options`\n\nCards are populated the same way: a data resource plus a client script on `DATA_FETCH_SUCCEEDED`\n\n.\n\nThe data resource, `look_up_multiple_records_1`\n\n, is a Composite \"Look up multiple records\" resource querying `incident`\n\nwith:\n\n`encodedQuery`\n\n:`active=true`\n\n`returnFields`\n\n:`active,short_description,description,priority,number`\n\n`limit`\n\n:`1000`\n\n,`sortType`\n\n:`asc`\n\nThe mapped client script (`SetCards`\n\n) maps each incident to a card object and writes the array to `cardsData`\n\n:\n\n```\n/**\n * @param {params} params\n * @param {api} params.api\n * @param {any} params.event\n * @param {any} params.imports\n * @param {ApiHelpers} params.helpers\n */\nfunction handler({\n    api,\n    event,\n    helpers,\n    imports\n}) {\n    var incidents = api.data.look_up_multiple_records_1.results;\n    var return_value = incidents.map(incident => {\n        return {\n            \"id\": incident._row_data.uniqueValue,\n            \"title\": incident.short_description.displayValue,\n            \"order\": 0,\n            \"lane_id\": \"lane_\" + incident.priority.value,\n            \"options\": {\n                \"board_type\": \"FREEFORM\",\n                \"card_type\": \"classic\",\n                \"due_date_field\": \"due_date\",\n                \"menu_actions\": [],\n                \"show_card_info\": true,\n                \"show_cover_image\": true,\n                \"show_labels\": true,\n                \"show_sla\": true\n            },\n\n            \"attachments\": [],\n            \"record\": {\n                \"short_description\": {\n                    \"display_value\": incident.short_description.value\n                },\n                \"description\": {\n                    \"display_value\": incident.description.value\n                },\n\n            },\n            \"labels\": [ \n                {\n                    \"active\": true, \n                    \"color\": \"#FFFF00\",\n                    \"value\": \"Yellow dot\"\n                }\n            ],\n            \"checklist\": {\n                \"items\": [{\n                    \"complete\": true\n                }]\n            },\n            \"sla\": {\n                \"percentage\": 23 \n            },\n            \"due_date_display_value\": \"31/12/2023\",\n        }\n    });\n    api.setState(\"cardsData\", return_value);\n}\n```\n\nNotice `\"lane_id\": \"lane_\" + incident.priority.value`\n\nmirrors the `id`\n\nwe built in `SetLanes`\n\n- this is what makes each incident's card render inside the lane matching its priority.\n\nCard and lane styling happens in two places, both visible in the scripts above:\n\n**Lane-level styling** via the`_style_`\n\nblock inside each lane object (set in`SetLanes`\n\n). The keys are CSS class names the component renders internally -`sn-lane`\n\nfor the lane body and`lane-footer-container`\n\nfor the sticky footer bar. We used this to fix each lane's width, add internal padding, enable vertical scrolling per lane, and cap lane height so the footer stays pinned (more on that in Step 5).**Card appearance** via the`options`\n\nand data fields inside each card object (set in`SetCards`\n\n):`card_type: \"classic\"`\n\nselects the fuller card layout (vs.`\"compact\"`\n\n).`show_card_info`\n\n,`show_cover_image`\n\n,`show_labels`\n\n,`show_sla`\n\ntoggle which visual regions of the card render.`labels`\n\ndrives the colored label chips/dots on the card (we set a yellow \"Yellow dot\" label as an example).`sla.percentage`\n\ndrives the SLA progress indicator.`checklist.items`\n\ndrives the checklist-complete badge.`due_date_display_value`\n\ndrives the due-date pill.\n\nFor the full list of supported `options`\n\nkeys on cards, lanes, cell headers, and swimlanes (colors, variants, tooltips, menu actions, etc.), see the [Part 2 configuration reference](https://www.servicenow.com/community/next-experience-articles/kanban-board-component-part-2-configuration-reference/ta-p/3057376) - we only used a subset of what's available.\n\nBy default a lane has no footer at all - the footer isn't just hidden, the component doesn't render one until you tell it which template to use. On the Kanban Board component's property panel, under **Lane footer configuration → Vertical lanes footer template**, set the value to `sn-vtb-lane-footer`\n\n. This is what actually switches the footer on; it corresponds to the `laneFooterTag`\n\nproperty on the component (visible in the page definition as `\"laneFooterTag\": \"sn-vtb-lane-footer\"`\n\n).\n\nWith that template wired up, the per-lane `options.can_add_task: true`\n\nwe set in `SetLanes`\n\ncontrols whether an individual lane's footer shows the **Add Card** affordance (a lane could have a footer template enabled at the board level but still opt out via `can_add_task: false`\n\n).\n\nWe paired this with the sticky-footer styling from Step 4 - `lane-footer-container: { position: \"sticky\", bottom: \"30px\" }`\n\ncombined with `sn-lane: { height: \"calc(100% - 58px)\", overflow-y: \"auto\" }`\n\n- so the footer stays pinned to the bottom of the lane while the cards inside scroll independently.\n\nThe footer's button text defaults to \"Add card\". We override it per lane with `add_card_footer_button_name`\n\nin the lane `options`\n\nobject - in our script we set it to `\"New Incident\"`\n\nto match the table the board represents:\n\n```\n\"options\": {\n    ...\n    \"can_add_task\": true,\n    \"add_card_footer_button_name\": \"New Incident\"\n}\n```\n\nClicking that button dispatches the board's `VTB#ADD_CARD_CLICKED`\n\naction with the triggering `card`\n\ncontext in the payload. On our page we mapped that action to open a modal containing a record-creation form for `incident`\n\n, using the `newIncidentSysID`\n\nstate parameter to pass the new record's sys_id into the form controller once it's created. That modal wiring is its own topic and outside the scope of this article, but it's the reason the button exists in the first place.\n\nWhen a card is dragged to a new lane or position, the board fires `sn_visual_board.NOW_VISUAL_BOARD#CARD_MOVED`\n\nwith a payload of `{ id, toLane, fromPosition, toPosition }`\n\n. We mapped a client script (`Move cards`\n\n) to that event which:\n\n- Re-sequences\n`order`\n\nfor both the destination lane and (if the card crossed lanes) the source lane, and writes the updated array back to`cardsData`\n\nso the UI reflects the move immediately. - Derives the incident's new\n`impact`\n\n/`urgency`\n\nfrom the destination lane's priority using a small lookup matrix. - Calls the\n`update_card`\n\nGraphQL data resource (configured with`readEvaluationMode: EXPLICIT`\n\nso it only runs when explicitly executed, not on every re-render) to persist`impact`\n\n,`urgency`\n\n, and a work note back to the incident record.\n\n```\n/**\n* @param {params} params\n* @param {api} params.api\n* @param {any} params.event\n* @param {any} params.imports\n* @param {ApiHelpers} params.helpers\n*/\nfunction handler({api, event, helpers, imports}) {\n    const { state: { cardsData } } = api;\n    const { payload: { id, toLane, fromPosition, toPosition } } = event;\n\n    // Work on a shallow copy so we don't mutate state directly\n    let cardsDataCopy = [...cardsData];\n\n    // Locate the card being moved\n    const movedCardIndex = cardsDataCopy.findIndex(c => c.id === id);\n    if (movedCardIndex === -1) {\n        return;\n    }\n\n    const movedCard = { ...cardsDataCopy[movedCardIndex] };\n    const fromLane = movedCard.lane_id;\n\n    // Pull it out of the working array\n    cardsDataCopy.splice(movedCardIndex, 1);\n\n    // Assign new lane\n    movedCard.lane_id = toLane;\n\n    // Cards currently in the destination lane, ordered\n    let destLaneCards = cardsDataCopy\n        .filter(c => c.lane_id === toLane)\n        .sort((a, b) => a.order - b.order);\n\n    // Insert moved card at its new position\n    destLaneCards.splice(toPosition, 0, movedCard);\n\n    // Re-sequence order values for destination lane\n    destLaneCards.forEach((c, idx) => { c.order = idx; });\n\n    // If it moved across lanes, re-sequence the source lane too\n    let sourceLaneCards = [];\n    if (fromLane !== toLane) {\n        sourceLaneCards = cardsDataCopy\n            .filter(c => c.lane_id === fromLane)\n            .sort((a, b) => a.order - b.order);\n        sourceLaneCards.forEach((c, idx) => { c.order = idx; });\n    }\n\n    // Everything untouched (other lanes)\n    const otherCards = cardsDataCopy.filter(\n        c => c.lane_id !== toLane && c.lane_id !== fromLane\n    );\n\n    const updatedCards = [...otherCards, ...sourceLaneCards, ...destLaneCards];\n\n    api.setState(\"cardsData\", updatedCards);\n\n    const priority = toLane.replace('lane_', '');\n    \n    const fields = getImpactUrgencyForPriority(priority);\n    if (!fields) {\n        console.warn('Unrecognised lane, skipping update:', toLane);\n        return;\n    }\n\n    api.data.update_card.execute({\n        \"table\": \"incident\",\n        \"recordId\": id,\n        \"templateFields\": \"impact=\" + fields.impact +\n                          \"^urgency=\" + fields.urgency +\n                          \"^work_notes=Change by moving cards\",\n        \"useDisplayValue\": false\n    });\n\n    function getImpactUrgencyForPriority(priority) {\n        const matrix = {\n            \"1\": { impact: \"1\", urgency: \"1\" },  // Critical\n            \"2\": { impact: \"1\", urgency: \"2\" },  // High\n            \"3\": { impact: \"2\", urgency: \"2\" },  // Moderate\n            \"4\": { impact: \"2\", urgency: \"3\" },  // Low\n            \"5\": { impact: \"3\", urgency: \"3\" }   // Planning\n        };\n        return matrix[String(priority)] || null;\n    }\n\n}\n```\n\nBecause `priority`\n\non the `incident`\n\ntable is derived from `impact`\n\nand `urgency`\n\nrather than being directly settable, this script reverse-maps the target lane back to an impact/urgency pair via a matrix, rather than writing to `priority`\n\ndirectly.\n\n**The** Lanes get an`lane_id`\n\ncontract is everything.`id`\n\nand cards get a matching`lane_id`\n\n; if the prefix/format ever diverges between your lanes script and your cards script, cards silently disappear from the board.**Lanes and cards are just state.** There's no dedicated \"lane designer\" UI - you populate`lanesData`\n\n/`cardsData`\n\nfrom any data resource and any transformation logic you want, which means the board can be driven by GlideRecord queries, Transforms, Scripted REST APIs, or GraphQL, not just the two examples here.**Most visual configuration lives in**, not in the Kanban component's own property panel. The`options`\n\nand`_style_`\n\non the lane/card objects`_style_`\n\nkeys map to internal CSS class names (`sn-lane`\n\n,`lane-footer-container`\n\n, etc.), so styling is really \"CSS-in-JSON\" per lane or card.The board is purely presentational until you map a script to`CARD_MOVED`\n\nis your write-back hook.`CARD_MOVED`\n\n(and optionally`LANE_MOVED`\n\n) to persist the reordering back to the source table.\n\nEverything the board can do beyond rendering lanes and cards — modals, archiving, drag-reordering, attachments, swimlane controls — is exposed as a fixed catalog of internal, \"hidden\" events on the `daActionMapping`\n\nproperty of the `Now Visual Board`\n\nmacroponent. We only wired up one of these ourselves (`VTB#ADD_CARD_CLICKED`\n\n, in Step 6), the same one shown as an example on the ServiceNow Community reference articles (`VTB#CONFIRMATION_MODAL_SELECTED`\n\n).\n\nTo document the rest, we pulled the `daActionMapping`\n\ndefinition from the Kanban Board component's own out-of-the-box demo page and extracted it in full — this is the component's built-in default, not something we authored, so it's a reliable reference for every hidden event the board supports and the exact payload shape each one dispatches.\n\nEach entry has the same shape: `assignmentId`\n\n(fixed, don't change it), `name`\n\n/`actionDispatch`\n\n(the identifier you dispatch a declarative action against), `actionType`\n\n(always `uxf_client_action`\n\n), `label`\n\n(what shows up in the UI Builder action picker), and `actionPayload`\n\n(a JSON template with `{{handlebars}}`\n\nplaceholders showing the fields available on the event).\n\n**A naming quirk to be aware of:** for three events — `CARD_MOVED`\n\n, `LANE_MOVED`\n\n, and `SWIMLANE_MOVED`\n\n— the *key* in the `daActionMapping`\n\nmap is prefixed `NOW_VISUAL_BOARD#...`\n\n, while the `name`\n\n/`actionDispatch`\n\nvalue inside it is `VTB#...`\n\n. Those three are also the ones exposed as public events on the component's standard **Events** tab, under the API name `sn_visual_board.NOW_VISUAL_BOARD#<EVENT>`\n\n— which is exactly the `sourceEventApiName`\n\nwe mapped our `Move cards`\n\nscript to in the Bonus section above. In other words, `CARD_MOVED`\n\ncan be handled either by mapping a script directly to the public event (what we did) or, if you're building your own declarative action/internal event chain the way `VTB#ADD_CARD_CLICKED`\n\nis wired on our page, via `daActionMapping`\n\n.\n\n| Action key | Dispatch name | Label | Payload fields |\n|---|---|---|---|\n`VTB#AMB_MESSAGE_RECEIVED` |\n`VTB_AMB_MESSAGE_RECIEVED` |\nAMB Message Recieved | `type` , `payload` |\n`VTB#LANE_HEADER_UPDATED` |\n`VTB_LANE_HEADER_UPDATED` |\nLane Header Updated | `updateLaneInput` |\n`VTB#CONFIRMATION_MODAL_SELECTED` |\n`VTB#CONFIRMATION_MODAL_SELECTED` |\nConfirmation Modal Selected | `modalId` , `modalData` |\n`VTB#SWIMLANE_HEADER_UPDATED` |\n`VTB_SWIMLANE_HEADER_UPDATED` |\nSwimlane Header Updated | `id` , `value` |\n`VTB#CARD_ARCHIVED` |\n`VTB#CARD_ARCHIVED` |\nCard Archived | `boardId` |\n`VTB#LANE_HIDE_SELECTED` |\n`VTB#LANE_HIDE_SELECTED` |\nLane Hide | `laneId` |\n`VTB#CARD_DETAILS_MODAL_CLOSED` |\n`VTB_CARD_DETAILS_MODAL_CLOSED` |\nCard Details Modal Closed | `value` |\n`VTB#LANE_DELETED` |\n`VTB#LANE_DELETED` |\nLane Deleted | `boardId` , `laneId` |\n`NOW_VISUAL_BOARD#LANE_MOVED` † |\n`VTB#LANE_MOVED` |\nLane Moved | `id` , `fromPosition` , `toPosition` |\n`VTB#ADD_CARD_CLICKED` |\n`VTB#ADD_CARD_CLICKED` |\nAdd Card Clicked | `card` (used in Step 6) |\n`VTB#CREATE_LANE_ACTION_PERFORMED` |\n`VTB_LANE_CREATED` |\nLane Created | `createLaneInput` |\n`VTB#FREEFORM_CARD_ADDED` |\n`VTB#FREEFORM_CARD_ADDED` |\nFreeform card added | `createCardInput` |\n`VTB#SWIMLANE_EXPAND_ALL` |\n`VTB#SWIMLANE_EXPAND_ALL` |\nSwimlane Expand All | (none) |\n`VTB#SWIMLANE_COLLAPSE_ALL` |\n`VTB#SWIMLANE_COLLAPSE_ALL` |\nSwimlane Collapse All | (none) |\n`VTB#ATTACHMENT_UPLOADED` |\n`VTB#ATTACHMENT_UPLOADED` |\nAttachment Uploaded | `type` , `action` , `sysId` , `table` , `loadAttachmentRecord` , `data` |\n`VTB#DATA_DRIVEN_CARD_ADDED` |\n`VTB#DATA_DRIVEN_CARD_ADDED` |\nData Driven Card Added | `boardId` , `taskId` , `swimlaneId` , `position` , `laneId` |\n`VTB#TOGGLE_SWIMLANE_BODY` |\n`VTB#TOGGLE_SWIMLANE_BODY` |\nSwimlane Body Toggle | (none) |\n`NOW_VISUAL_BOARD#SWIMLANE_MOVED` † |\n`VTB#SWIMLANE_MOVED` |\nSwimlane Moved | `id` , `fromPosition` , `toPosition` |\n`VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED` |\n`VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED` |\nFreeform placeholder card removed | (none) |\n`VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED` |\n`VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED` |\nData Driven Card Form Modal Closed | (none) |\n`NOW_VISUAL_BOARD#CARD_MOVED` † |\n`VTB#CARD_MOVED` |\nCard Moved | `id` , `fromLane` , `toLane` , `fromSwimlane` , `toSwimlane` , `fromPosition` , `toPosition` (see Bonus section) |\n`VTB#WRAPPER_DOM_ACTION_PERFORMED` |\n`VTB#WRAPPER_DOM_ACTION_PERFORMED` |\nWrapper DOM action performed | `event` , `name` |\n`VTB#SWIMLANE_ACTION_PERFORMED` |\n`VTB#SWIMLANE_ACTION_PERFORMED` |\nSwimlane Action Performed | `payload` , `type` |\n\n† Map key uses the `NOW_VISUAL_BOARD#`\n\nprefix instead of `VTB#`\n\n; also available as a public event on the Events tab (`sn_visual_board.NOW_VISUAL_BOARD#<EVENT>`\n\n).\n\nExtracted verbatim from the component's out-of-the-box demo page (assignment IDs included) so it can be cross-checked field-for-field against your own instance:\n\n```\n{\n  \"VTB#AMB_MESSAGE_RECEIVED\": {\n    \"actionDispatch\": \"VTB_AMB_MESSAGE_RECIEVED\",\n    \"actionPayload\": \"{\\r\\n        \\\"type\\\": \\\"{{type}}\\\",\\r\\n        \\\"payload\\\": \\\"{{payload}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"0152e2f453922010c5e2ddeeff7b121c\",\n    \"label\": \"AMB Message Recieved\",\n    \"name\": \"VTB_AMB_MESSAGE_RECIEVED\"\n  },\n  \"VTB#LANE_HEADER_UPDATED\": {\n    \"actionDispatch\": \"VTB_LANE_HEADER_UPDATED\",\n    \"actionPayload\": \"{\\r\\n        \\\"updateLaneInput\\\": \\\"{{updateLaneInput}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"03fdd23453922010c5e2ddeeff7b121c\",\n    \"label\": \"Lane Header Updated\",\n    \"name\": \"VTB_LANE_HEADER_UPDATED\"\n  },\n  \"VTB#CONFIRMATION_MODAL_SELECTED\": {\n    \"actionDispatch\": \"VTB#CONFIRMATION_MODAL_SELECTED\",\n    \"actionPayload\": \"{\\r\\n        \\\"modalId\\\": \\\"{{modalId}}\\\",\\r\\n        \\\"modalData\\\": \\\"{{modalData}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"0c8e127453922010c5e2ddeeff7b125f\",\n    \"label\": \"Confirmation Modal Selected\",\n    \"name\": \"VTB#CONFIRMATION_MODAL_SELECTED\"\n  },\n  \"VTB#SWIMLANE_HEADER_UPDATED\": {\n    \"actionDispatch\": \"VTB_SWIMLANE_HEADER_UPDATED\",\n    \"actionPayload\": \"{\\r\\n        \\\"id\\\": \\\"{{id}}\\\",\\r\\n        \\\"value\\\": \\\"{{value}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"1c0b9eb053922010c5e2ddeeff7b1202\",\n    \"label\": \"Swimlane Header Updated\",\n    \"name\": \"VTB_SWIMLANE_HEADER_UPDATED\"\n  },\n  \"VTB#CARD_ARCHIVED\": {\n    \"actionDispatch\": \"VTB#CARD_ARCHIVED\",\n    \"actionPayload\": \"{\\r\\n        \\\"boardId\\\": \\\"{{boardId}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"228a5ab053922010c5e2ddeeff7b1260\",\n    \"label\": \"Card Archived\",\n    \"name\": \"VTB#CARD_ARCHIVED\"\n  },\n  \"VTB#LANE_HIDE_SELECTED\": {\n    \"actionDispatch\": \"VTB#LANE_HIDE_SELECTED\",\n    \"actionPayload\": \"{\\r\\n        \\\"laneId\\\": \\\"{{laneId}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"3e816eb453922010c5e2ddeeff7b12f1\",\n    \"label\": \"Lane Hide\",\n    \"name\": \"VTB#LANE_HIDE_SELECTED\"\n  },\n  \"VTB#CARD_DETAILS_MODAL_CLOSED\": {\n    \"actionDispatch\": \"VTB_CARD_DETAILS_MODAL_CLOSED\",\n    \"actionPayload\": \"{\\r\\n        \\\"value\\\": \\\"{{value}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"4492e6f453922010c5e2ddeeff7b1269\",\n    \"label\": \"Card Details Modal Closed\",\n    \"name\": \"VTB_CARD_DETAILS_MODAL_CLOSED\"\n  },\n  \"VTB#LANE_DELETED\": {\n    \"actionDispatch\": \"VTB#LANE_DELETED\",\n    \"actionPayload\": \"{\\r\\n        \\\"boardId\\\": \\\"{{boardId}}\\\",\\r\\n        \\\"laneId\\\": \\\"{{laneId}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"4a012ab453922010c5e2ddeeff7b1282\",\n    \"label\": \"Lane Deleted\",\n    \"name\": \"VTB#LANE_DELETED\"\n  },\n  \"NOW_VISUAL_BOARD#LANE_MOVED\": {\n    \"actionDispatch\": \"VTB#LANE_MOVED\",\n    \"actionPayload\": \"{\\r\\n        \\\"id\\\": \\\"{{id}}\\\",\\r\\n        \\\"fromPosition\\\": \\\"{{fromPosition}}\\\",\\r\\n         \\\"toPosition\\\": \\\"{{toPosition}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"4b3e5e3453922010c5e2ddeeff7b1246\",\n    \"label\": \"Lane Moved\",\n    \"name\": \"VTB#LANE_MOVED\"\n  },\n  \"VTB#ADD_CARD_CLICKED\": {\n    \"actionDispatch\": \"VTB#ADD_CARD_CLICKED\",\n    \"actionPayload\": \"{\\r\\n        \\\"card\\\": \\\"{{card}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"4dc2aaf453922010c5e2ddeeff7b12c8\",\n    \"label\": \"Add Card Clicked\",\n    \"name\": \"VTB#ADD_CARD_CLICKED\"\n  },\n  \"VTB#CREATE_LANE_ACTION_PERFORMED\": {\n    \"actionDispatch\": \"VTB_LANE_CREATED\",\n    \"actionPayload\": \"{\\r\\n        \\\"createLaneInput\\\": \\\"{{createLaneInput}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"4fc81a7053922010c5e2ddeeff7b12e4\",\n    \"label\": \"Lane Created\",\n    \"name\": \"VTB_LANE_CREATED\"\n  },\n  \"VTB#FREEFORM_CARD_ADDED\": {\n    \"actionDispatch\": \"VTB#FREEFORM_CARD_ADDED\",\n    \"actionPayload\": \"{\\r\\n        \\\"createCardInput\\\": \\\"{{createCardInput}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"5a9026b453922010c5e2ddeeff7b120a\",\n    \"label\": \"Freeform card added\",\n    \"name\": \"VTB#FREEFORM_CARD_ADDED\"\n  },\n  \"VTB#SWIMLANE_EXPAND_ALL\": {\n    \"actionDispatch\": \"VTB#SWIMLANE_EXPAND_ALL\",\n    \"actionPayload\": null,\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"5ce162f453922010c5e2ddeeff7b1216\",\n    \"label\": \"Swimlane Expand All\",\n    \"name\": \"VTB#SWIMLANE_EXPAND_ALL\"\n  },\n  \"VTB#SWIMLANE_COLLAPSE_ALL\": {\n    \"actionDispatch\": \"VTB#SWIMLANE_COLLAPSE_ALL\",\n    \"actionPayload\": null,\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"661d923453922010c5e2ddeeff7b12d5\",\n    \"label\": \"Swimlane Collapse All\",\n    \"name\": \"VTB#SWIMLANE_COLLAPSE_ALL\"\n  },\n  \"VTB#ATTACHMENT_UPLOADED\": {\n    \"actionDispatch\": \"VTB#ATTACHMENT_UPLOADED\",\n    \"actionPayload\": \"{\\r\\n        \\\"type\\\": \\\"{{sysparm_type}}\\\",\\r\\n         \\\"action\\\": \\\"{{action}}\\\",\\r\\n         \\\"sysId\\\": \\\"{{sys_id}}\\\",\\r\\n         \\\"table\\\": \\\"{{table}}\\\",\\r\\n        \\\"loadAttachmentRecord\\\": \\\"{{load_attachment_record}}\\\",\\r\\n        \\\"data\\\": \\\"{{data}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"88fbd6f053922010c5e2ddeeff7b12d2\",\n    \"label\": \"Attachment Uploaded\",\n    \"name\": \"VTB#ATTACHMENT_UPLOADED\"\n  },\n  \"VTB#DATA_DRIVEN_CARD_ADDED\": {\n    \"actionDispatch\": \"VTB#DATA_DRIVEN_CARD_ADDED\",\n    \"actionPayload\": \"{\\r\\n        \\\"boardId\\\": \\\"{{boardId}}\\\",\\r\\n        \\\"taskId\\\": \\\"{{taskId}}\\\",\\r\\n        \\\"swimlaneId\\\": \\\"{{swimlaneId}}\\\",\\r\\n        \\\"position\\\": \\\"{{position}}\\\",\\r\\n        \\\"laneId\\\": \\\"{{laneId}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"9820e67453922010c5e2ddeeff7b1208\",\n    \"label\": \"Data Driven Card Added\",\n    \"name\": \"VTB#DATA_DRIVEN_CARD_ADDED\"\n  },\n  \"VTB#TOGGLE_SWIMLANE_BODY\": {\n    \"actionDispatch\": \"VTB#TOGGLE_SWIMLANE_BODY\",\n    \"actionPayload\": null,\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"b202a2f453922010c5e2ddeeff7b1260\",\n    \"label\": \"Swimlane Body Toggle\",\n    \"name\": \"VTB#TOGGLE_SWIMLANE_BODY\"\n  },\n  \"NOW_VISUAL_BOARD#SWIMLANE_MOVED\": {\n    \"actionDispatch\": \"VTB#SWIMLANE_MOVED\",\n    \"actionPayload\": \"{\\r\\n        \\\"id\\\": \\\"{{id}}\\\",\\r\\n        \\\"fromPosition\\\": \\\"{{fromPosition}}\\\",\\r\\n        \\\"toPosition\\\": \\\"{{toPosition}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"c33a16b053922010c5e2ddeeff7b1217\",\n    \"label\": \"Swimlane Moved\",\n    \"name\": \"VTB#SWIMLANE_MOVED\"\n  },\n  \"VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED\": {\n    \"actionDispatch\": \"VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED\",\n    \"actionPayload\": null,\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"decc123453922010c5e2ddeeff7b1243\",\n    \"label\": \"Freeform placeholder card removed\",\n    \"name\": \"VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED\"\n  },\n  \"VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED\": {\n    \"actionDispatch\": \"VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED\",\n    \"actionPayload\": null,\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"dee992b053922010c5e2ddeeff7b12c3\",\n    \"label\": \"Data Driven Card Form Modal Closed\",\n    \"name\": \"VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED\"\n  },\n  \"NOW_VISUAL_BOARD#CARD_MOVED\": {\n    \"actionDispatch\": \"VTB#CARD_MOVED\",\n    \"actionPayload\": \"{\\r\\n        \\\"id\\\": \\\"{{id}}\\\",\\r\\n        \\\"fromLane\\\": \\\"{{fromLane}}\\\",\\r\\n        \\\"toLane\\\": \\\"{{toLane}}\\\",\\r\\n        \\\"fromSwimlane\\\": \\\"{{fromSwimlane}}\\\",\\r\\n        \\\"toSwimlane\\\": \\\"{{toSwimlane}}\\\",\\r\\n        \\\"fromPosition\\\": \\\"{{fromPosition}}\\\",\\r\\n        \\\"toPosition\\\": \\\"{{toPosition}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"7dab16f053922010c5e2ddeeff7b122a\",\n    \"label\": \"Card Moved\",\n    \"name\": \"VTB#CARD_MOVED\"\n  },\n  \"VTB#WRAPPER_DOM_ACTION_PERFORMED\": {\n    \"actionDispatch\": \"VTB#WRAPPER_DOM_ACTION_PERFORMED\",\n    \"actionPayload\": \"{\\r\\n        \\\"event\\\": \\\"{{event}}\\\",\\r\\n        \\\"name\\\": \\\"{{name}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"7a40e516b7122010c5e229a6ee11a937\",\n    \"label\": \"Wrapper DOM action performed\",\n    \"name\": \"VTB#WRAPPER_DOM_ACTION_PERFORMED\"\n  },\n  \"VTB#SWIMLANE_ACTION_PERFORMED\": {\n    \"actionDispatch\": \"VTB#SWIMLANE_ACTION_PERFORMED\",\n    \"actionPayload\": \"{\\r\\n        \\\"payload\\\": \\\"{{payload}}\\\",\\r\\n        \\\"type\\\": \\\"{{type}}\\\"\\r\\n}\",\n    \"actionType\": \"uxf_client_action\",\n    \"assignmentId\": \"f518127053922010c5e2ddeeff7b125a\",\n    \"label\": \"Swimlane Action Performed\",\n    \"name\": \"VTB#SWIMLANE_ACTION_PERFORMED\"\n  }\n}\n```\n\n", "url": "https://wpnews.pro/news/using-the-kanban-board-component-in-ui-builder", "canonical_source": "https://gist.github.com/kunalkhatri/0b79587929a77d1a6582b9b9785b75ab", "published_at": "2026-08-14 09:51:25+00:00", "updated_at": "2026-08-15 07:12:15.051847+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Kunal Khatri", "ServiceNow", "UI Builder", "Kanban Board", "sn-vtb"], "alternates": {"html": "https://wpnews.pro/news/using-the-kanban-board-component-in-ui-builder", "markdown": "https://wpnews.pro/news/using-the-kanban-board-component-in-ui-builder.md", "text": "https://wpnews.pro/news/using-the-kanban-board-component-in-ui-builder.txt", "jsonld": "https://wpnews.pro/news/using-the-kanban-board-component-in-ui-builder.jsonld"}}