cd /news/developer-tools/using-the-kanban-board-component-in-… · home topics developer-tools article
[ARTICLE · art-97723] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Using the Kanban Board Component in UI Builder

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.

read17 min views2 publishedAug 14, 2026

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.

This 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.

It 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 at the end.

The board in this article is bound to the incident

table and groups incidents into lanes by Priority. Everything is driven by three UI Builder building blocks:

Element Type Purpose
get_priority_values_1
Transform data resource Fetches the Priority choice list for incident , used to generate one lane per priority value
look_up_multiple_records_1
Composite ("Look up multiple records") data resource Fetches active incidents (active=true , up to 1000 rows) with the fields needed for the cards
update_card
GraphQL data resource (explicit/manual execution) Writes impact /urgency /work_notes back to the incident when a card is dragged to a new lane
lanesData
Client State Parameter (JSON) Backing array for the board's lanes property
cardsData
Client State Parameter (JSON) Backing array for the board's cards property
newIncidentSysID
Client State Parameter (string) Feeds the sys_id of a newly created incident into a record-creation modal
kanban_board_1
Kanban Board (sn-vtb ) macroponent
The board itself, with lanes and cards bound to the two state parameters above

Two client scripts run automatically when their data resource fetches succeed (SetLanes

, SetCards

), and one runs in response to the board's CARD_MOVED

event (Move cards

). All three are reproduced in full below, at the step where they're relevant.

  • Open the page in UI Builder and drag the Kanban Board component from the component picker onto the canvas. - Before you can bind any data to it, create two Client State Parameters of typejson

:lanesData

  • initial valuenull

cardsData

  • initial valuenull

  • Select the Kanban Board component and bind its properties: lanes

State BindinglanesData

cards

State BindingcardsData

At this point the board renders but is empty - nothing has populated lanesData

or cardsData

yet. That's what the next two steps do.

The Kanban Board component ships as part of the

sn-vtb

(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].

Lanes come from data, not from manual configuration in the UI Builder canvas. We added a Transform data resource, get_priority_values_1

, that returns the Priority choice list for the incident

table (an array of { sys_id, label, value }

objects).

A 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

. Ours queries sys_choice

for the task

table's priority

element, ordered by value:

function transform(){
    let choice_list = [];
    var gr = new GlideRecord("sys_choice");
    gr.addEncodedQuery("language=en^name=task^elementSTARTSWITHpriority");
    gr.orderBy("value");
    gr.query();
    while(gr.next()){
        choice_list.push({
            "sys_id":gr.getUniqueValue(),
            "label":gr.getValue("label"),
            "value":gr.getValue("value")
        });
    }
    return choice_list;
}

A couple of notes on this query:

  • We query sys_choice

onname=task

(notname=incident

) becausepriority

is defined on the basetask

table and inherited byincident

  • queryingtask

picks up the choice list regardless of which task-derived table you point the board at. elementSTARTSWITHpriority

matches thepriority

element; usingSTARTSWITH

rather than an exact match is a defensive habit here, though an exactelement=priority

would work identically for this field.orderBy("value")

gives us the choices in priority order (1–Critical through 5–Planning), which becomes the left-to-right lane order on the board sinceSetLanes

below just maps the array in place.

This 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

event.

We mapped a client script to that event. This script (SetLanes

) turns each priority choice into a lane object and writes the result to lanesData

:

/**
* @param {params} params
* @param {api} params.api
* @param {any} params.event
* @param {any} params.imports
* @param {ApiHelpers} params.helpers
*/
function handler({api, event, helpers, imports}) {
    var data = api.data.get_priority_values_1.output;
    var return_value = data.map(choice=>{
        return {
            "id": "lane_"+choice.value,
            "title": choice.label,
            "name": choice.label,
            "_style_": {
                "sn-lane": {
                    "width": "285px",
                    "padding": "3px",
                    "overflow-y": "auto",
                    "height": "calc(100% - 58px)"
                },
                "lane-footer-container": {
                    "position": "sticky",
                    "bottom": "30px"
                }
            },
            "options": {
                "board_type": "FREEFORM",
                "is2DBoard": false,
                "lane_filter": "",
                "lane_count": 3,
                "total_lane_count": 3,
                "lane_field": "__KANBAN__",
                "swimlane_field": "",
                "table": "incident",
                "highest_lane_order": 2,
                "can_add_task": true,
                "add_card_footer_button_name": "New Incident"
            }
        }
    });
    api.setState("lanesData",return_value);

}

A couple of things worth calling out:

  • every lane'sid: "lane_" + choice.value

id

is derived from the priority value (e.g.lane_1

,lane_2

, …). We use this exact prefix again in the card script, so cards land in the correct lane. Thislane_id

contract between the two scripts is the single most important detail to get right.and_style_

are set per lane here - see Steps 4–6 below for what each of those keys does.options

Cards are populated the same way: a data resource plus a client script on DATA_FETCH_SUCCEEDED

.

The data resource, look_up_multiple_records_1

, is a Composite "Look up multiple records" resource querying incident

with:

encodedQuery

:active=true

returnFields

:active,short_description,description,priority,number

limit

:1000

,sortType

:asc

The mapped client script (SetCards

) maps each incident to a card object and writes the array to cardsData

:

/**
 * @param {params} params
 * @param {api} params.api
 * @param {any} params.event
 * @param {any} params.imports
 * @param {ApiHelpers} params.helpers
 */
function handler({
    api,
    event,
    helpers,
    imports
}) {
    var incidents = api.data.look_up_multiple_records_1.results;
    var return_value = incidents.map(incident => {
        return {
            "id": incident._row_data.uniqueValue,
            "title": incident.short_description.displayValue,
            "order": 0,
            "lane_id": "lane_" + incident.priority.value,
            "options": {
                "board_type": "FREEFORM",
                "card_type": "classic",
                "due_date_field": "due_date",
                "menu_actions": [],
                "show_card_info": true,
                "show_cover_image": true,
                "show_labels": true,
                "show_sla": true
            },

            "attachments": [],
            "record": {
                "short_description": {
                    "display_value": incident.short_description.value
                },
                "description": {
                    "display_value": incident.description.value
                },

            },
            "labels": [ 
                {
                    "active": true, 
                    "color": "#FFFF00",
                    "value": "Yellow dot"
                }
            ],
            "checklist": {
                "items": [{
                    "complete": true
                }]
            },
            "sla": {
                "percentage": 23 
            },
            "due_date_display_value": "31/12/2023",
        }
    });
    api.setState("cardsData", return_value);
}

Notice "lane_id": "lane_" + incident.priority.value

mirrors the id

we built in SetLanes

  • this is what makes each incident's card render inside the lane matching its priority.

Card and lane styling happens in two places, both visible in the scripts above:

Lane-level styling via the_style_

block inside each lane object (set inSetLanes

). The keys are CSS class names the component renders internally -sn-lane

for the lane body andlane-footer-container

for 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 theoptions

and data fields inside each card object (set inSetCards

):card_type: "classic"

selects the fuller card layout (vs."compact"

).show_card_info

,show_cover_image

,show_labels

,show_sla

toggle which visual regions of the card render.labels

drives the colored label chips/dots on the card (we set a yellow "Yellow dot" label as an example).sla.percentage

drives the SLA progress indicator.checklist.items

drives the checklist-complete badge.due_date_display_value

drives the due-date pill.

For the full list of supported options

keys on cards, lanes, cell headers, and swimlanes (colors, variants, tooltips, menu actions, etc.), see the Part 2 configuration reference - we only used a subset of what's available.

By 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

. This is what actually switches the footer on; it corresponds to the laneFooterTag

property on the component (visible in the page definition as "laneFooterTag": "sn-vtb-lane-footer"

).

With that template wired up, the per-lane options.can_add_task: true

we set in SetLanes

controls 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

).

We paired this with the sticky-footer styling from Step 4 - lane-footer-container: { position: "sticky", bottom: "30px" }

combined with sn-lane: { height: "calc(100% - 58px)", overflow-y: "auto" }

  • so the footer stays pinned to the bottom of the lane while the cards inside scroll independently.

The footer's button text defaults to "Add card". We override it per lane with add_card_footer_button_name

in the lane options

object - in our script we set it to "New Incident"

to match the table the board represents:

"options": {
    ...
    "can_add_task": true,
    "add_card_footer_button_name": "New Incident"
}

Clicking that button dispatches the board's VTB#ADD_CARD_CLICKED

action with the triggering card

context in the payload. On our page we mapped that action to open a modal containing a record-creation form for incident

, using the newIncidentSysID

state 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.

When a card is dragged to a new lane or position, the board fires sn_visual_board.NOW_VISUAL_BOARD#CARD_MOVED

with a payload of { id, toLane, fromPosition, toPosition }

. We mapped a client script (Move cards

) to that event which:

  • Re-sequences order

for both the destination lane and (if the card crossed lanes) the source lane, and writes the updated array back tocardsData

so the UI reflects the move immediately. - Derives the incident's new impact

/urgency

from the destination lane's priority using a small lookup matrix. - Calls the update_card

GraphQL data resource (configured withreadEvaluationMode: EXPLICIT

so it only runs when explicitly executed, not on every re-render) to persistimpact

,urgency

, and a work note back to the incident record.

/**
* @param {params} params
* @param {api} params.api
* @param {any} params.event
* @param {any} params.imports
* @param {ApiHelpers} params.helpers
*/
function handler({api, event, helpers, imports}) {
    const { state: { cardsData } } = api;
    const { payload: { id, toLane, fromPosition, toPosition } } = event;

    // Work on a shallow copy so we don't mutate state directly
    let cardsDataCopy = [...cardsData];

    // Locate the card being moved
    const movedCardIndex = cardsDataCopy.findIndex(c => c.id === id);
    if (movedCardIndex === -1) {
        return;
    }

    const movedCard = { ...cardsDataCopy[movedCardIndex] };
    const fromLane = movedCard.lane_id;

    // Pull it out of the working array
    cardsDataCopy.splice(movedCardIndex, 1);

    // Assign new lane
    movedCard.lane_id = toLane;

    // Cards currently in the destination lane, ordered
    let destLaneCards = cardsDataCopy
        .filter(c => c.lane_id === toLane)
        .sort((a, b) => a.order - b.order);

    // Insert moved card at its new position
    destLaneCards.splice(toPosition, 0, movedCard);

    // Re-sequence order values for destination lane
    destLaneCards.forEach((c, idx) => { c.order = idx; });

    // If it moved across lanes, re-sequence the source lane too
    let sourceLaneCards = [];
    if (fromLane !== toLane) {
        sourceLaneCards = cardsDataCopy
            .filter(c => c.lane_id === fromLane)
            .sort((a, b) => a.order - b.order);
        sourceLaneCards.forEach((c, idx) => { c.order = idx; });
    }

    // Everything untouched (other lanes)
    const otherCards = cardsDataCopy.filter(
        c => c.lane_id !== toLane && c.lane_id !== fromLane
    );

    const updatedCards = [...otherCards, ...sourceLaneCards, ...destLaneCards];

    api.setState("cardsData", updatedCards);

    const priority = toLane.replace('lane_', '');
    
    const fields = getImpactUrgencyForPriority(priority);
    if (!fields) {
        console.warn('Unrecognised lane, skipping update:', toLane);
        return;
    }

    api.data.update_card.execute({
        "table": "incident",
        "recordId": id,
        "templateFields": "impact=" + fields.impact +
                          "^urgency=" + fields.urgency +
                          "^work_notes=Change by moving cards",
        "useDisplayValue": false
    });

    function getImpactUrgencyForPriority(priority) {
        const matrix = {
            "1": { impact: "1", urgency: "1" },  // Critical
            "2": { impact: "1", urgency: "2" },  // High
            "3": { impact: "2", urgency: "2" },  // Moderate
            "4": { impact: "2", urgency: "3" },  // Low
            "5": { impact: "3", urgency: "3" }   // Planning
        };
        return matrix[String(priority)] || null;
    }

}

Because priority

on the incident

table is derived from impact

and urgency

rather 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

directly.

The Lanes get anlane_id

contract is everything.id

and cards get a matchinglane_id

; 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 populatelanesData

/cardsData

from 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. Theoptions

and_style_

on the lane/card objects_style_

keys map to internal CSS class names (sn-lane

,lane-footer-container

, etc.), so styling is really "CSS-in-JSON" per lane or card.The board is purely presentational until you map a script toCARD_MOVED

is your write-back hook.CARD_MOVED

(and optionallyLANE_MOVED

) to persist the reordering back to the source table.

Everything 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

property of the Now Visual Board

macroponent. We only wired up one of these ourselves (VTB#ADD_CARD_CLICKED

, in Step 6), the same one shown as an example on the ServiceNow Community reference articles (VTB#CONFIRMATION_MODAL_SELECTED

).

To document the rest, we pulled the daActionMapping

definition 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.

Each entry has the same shape: assignmentId

(fixed, don't change it), name

/actionDispatch

(the identifier you dispatch a declarative action against), actionType

(always uxf_client_action

), label

(what shows up in the UI Builder action picker), and actionPayload

(a JSON template with {{handlebars}}

placeholders showing the fields available on the event).

A naming quirk to be aware of: for three events — CARD_MOVED

, LANE_MOVED

, and SWIMLANE_MOVED

— the key in the daActionMapping

map is prefixed NOW_VISUAL_BOARD#...

, while the name

/actionDispatch

value inside it is VTB#...

. 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>

— which is exactly the sourceEventApiName

we mapped our Move cards

script to in the Bonus section above. In other words, CARD_MOVED

can 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

is wired on our page, via daActionMapping

.

Action key Dispatch name Label Payload fields
VTB#AMB_MESSAGE_RECEIVED
VTB_AMB_MESSAGE_RECIEVED
AMB Message Recieved type , payload
VTB#LANE_HEADER_UPDATED
VTB_LANE_HEADER_UPDATED
Lane Header Updated updateLaneInput
VTB#CONFIRMATION_MODAL_SELECTED
VTB#CONFIRMATION_MODAL_SELECTED
Confirmation Modal Selected modalId , modalData
VTB#SWIMLANE_HEADER_UPDATED
VTB_SWIMLANE_HEADER_UPDATED
Swimlane Header Updated id , value
VTB#CARD_ARCHIVED
VTB#CARD_ARCHIVED
Card Archived boardId
VTB#LANE_HIDE_SELECTED
VTB#LANE_HIDE_SELECTED
Lane Hide laneId
VTB#CARD_DETAILS_MODAL_CLOSED
VTB_CARD_DETAILS_MODAL_CLOSED
Card Details Modal Closed value
VTB#LANE_DELETED
VTB#LANE_DELETED
Lane Deleted boardId , laneId
NOW_VISUAL_BOARD#LANE_MOVED
VTB#LANE_MOVED
Lane Moved id , fromPosition , toPosition
VTB#ADD_CARD_CLICKED
VTB#ADD_CARD_CLICKED
Add Card Clicked card (used in Step 6)
VTB#CREATE_LANE_ACTION_PERFORMED
VTB_LANE_CREATED
Lane Created createLaneInput
VTB#FREEFORM_CARD_ADDED
VTB#FREEFORM_CARD_ADDED
Freeform card added createCardInput
VTB#SWIMLANE_EXPAND_ALL
VTB#SWIMLANE_EXPAND_ALL
Swimlane Expand All (none)
VTB#SWIMLANE_COLLAPSE_ALL
VTB#SWIMLANE_COLLAPSE_ALL
Swimlane Collapse All (none)
VTB#ATTACHMENT_UPLOADED
VTB#ATTACHMENT_UPLOADED
Attachment Uploaded type , action , sysId , table , loadAttachmentRecord , data
VTB#DATA_DRIVEN_CARD_ADDED
VTB#DATA_DRIVEN_CARD_ADDED
Data Driven Card Added boardId , taskId , swimlaneId , position , laneId
VTB#TOGGLE_SWIMLANE_BODY
VTB#TOGGLE_SWIMLANE_BODY
Swimlane Body Toggle (none)
NOW_VISUAL_BOARD#SWIMLANE_MOVED
VTB#SWIMLANE_MOVED
Swimlane Moved id , fromPosition , toPosition
VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED
VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED
Freeform placeholder card removed (none)
VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED
VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED
Data Driven Card Form Modal Closed (none)
NOW_VISUAL_BOARD#CARD_MOVED
VTB#CARD_MOVED
Card Moved id , fromLane , toLane , fromSwimlane , toSwimlane , fromPosition , toPosition (see Bonus section)
VTB#WRAPPER_DOM_ACTION_PERFORMED
VTB#WRAPPER_DOM_ACTION_PERFORMED
Wrapper DOM action performed event , name
VTB#SWIMLANE_ACTION_PERFORMED
VTB#SWIMLANE_ACTION_PERFORMED
Swimlane Action Performed payload , type

† Map key uses the NOW_VISUAL_BOARD#

prefix instead of VTB#

; also available as a public event on the Events tab (sn_visual_board.NOW_VISUAL_BOARD#<EVENT>

).

Extracted 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:

{
  "VTB#AMB_MESSAGE_RECEIVED": {
    "actionDispatch": "VTB_AMB_MESSAGE_RECIEVED",
    "actionPayload": "{\r\n        \"type\": \"{{type}}\",\r\n        \"payload\": \"{{payload}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "0152e2f453922010c5e2ddeeff7b121c",
    "label": "AMB Message Recieved",
    "name": "VTB_AMB_MESSAGE_RECIEVED"
  },
  "VTB#LANE_HEADER_UPDATED": {
    "actionDispatch": "VTB_LANE_HEADER_UPDATED",
    "actionPayload": "{\r\n        \"updateLaneInput\": \"{{updateLaneInput}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "03fdd23453922010c5e2ddeeff7b121c",
    "label": "Lane Header Updated",
    "name": "VTB_LANE_HEADER_UPDATED"
  },
  "VTB#CONFIRMATION_MODAL_SELECTED": {
    "actionDispatch": "VTB#CONFIRMATION_MODAL_SELECTED",
    "actionPayload": "{\r\n        \"modalId\": \"{{modalId}}\",\r\n        \"modalData\": \"{{modalData}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "0c8e127453922010c5e2ddeeff7b125f",
    "label": "Confirmation Modal Selected",
    "name": "VTB#CONFIRMATION_MODAL_SELECTED"
  },
  "VTB#SWIMLANE_HEADER_UPDATED": {
    "actionDispatch": "VTB_SWIMLANE_HEADER_UPDATED",
    "actionPayload": "{\r\n        \"id\": \"{{id}}\",\r\n        \"value\": \"{{value}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "1c0b9eb053922010c5e2ddeeff7b1202",
    "label": "Swimlane Header Updated",
    "name": "VTB_SWIMLANE_HEADER_UPDATED"
  },
  "VTB#CARD_ARCHIVED": {
    "actionDispatch": "VTB#CARD_ARCHIVED",
    "actionPayload": "{\r\n        \"boardId\": \"{{boardId}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "228a5ab053922010c5e2ddeeff7b1260",
    "label": "Card Archived",
    "name": "VTB#CARD_ARCHIVED"
  },
  "VTB#LANE_HIDE_SELECTED": {
    "actionDispatch": "VTB#LANE_HIDE_SELECTED",
    "actionPayload": "{\r\n        \"laneId\": \"{{laneId}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "3e816eb453922010c5e2ddeeff7b12f1",
    "label": "Lane Hide",
    "name": "VTB#LANE_HIDE_SELECTED"
  },
  "VTB#CARD_DETAILS_MODAL_CLOSED": {
    "actionDispatch": "VTB_CARD_DETAILS_MODAL_CLOSED",
    "actionPayload": "{\r\n        \"value\": \"{{value}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "4492e6f453922010c5e2ddeeff7b1269",
    "label": "Card Details Modal Closed",
    "name": "VTB_CARD_DETAILS_MODAL_CLOSED"
  },
  "VTB#LANE_DELETED": {
    "actionDispatch": "VTB#LANE_DELETED",
    "actionPayload": "{\r\n        \"boardId\": \"{{boardId}}\",\r\n        \"laneId\": \"{{laneId}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "4a012ab453922010c5e2ddeeff7b1282",
    "label": "Lane Deleted",
    "name": "VTB#LANE_DELETED"
  },
  "NOW_VISUAL_BOARD#LANE_MOVED": {
    "actionDispatch": "VTB#LANE_MOVED",
    "actionPayload": "{\r\n        \"id\": \"{{id}}\",\r\n        \"fromPosition\": \"{{fromPosition}}\",\r\n         \"toPosition\": \"{{toPosition}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "4b3e5e3453922010c5e2ddeeff7b1246",
    "label": "Lane Moved",
    "name": "VTB#LANE_MOVED"
  },
  "VTB#ADD_CARD_CLICKED": {
    "actionDispatch": "VTB#ADD_CARD_CLICKED",
    "actionPayload": "{\r\n        \"card\": \"{{card}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "4dc2aaf453922010c5e2ddeeff7b12c8",
    "label": "Add Card Clicked",
    "name": "VTB#ADD_CARD_CLICKED"
  },
  "VTB#CREATE_LANE_ACTION_PERFORMED": {
    "actionDispatch": "VTB_LANE_CREATED",
    "actionPayload": "{\r\n        \"createLaneInput\": \"{{createLaneInput}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "4fc81a7053922010c5e2ddeeff7b12e4",
    "label": "Lane Created",
    "name": "VTB_LANE_CREATED"
  },
  "VTB#FREEFORM_CARD_ADDED": {
    "actionDispatch": "VTB#FREEFORM_CARD_ADDED",
    "actionPayload": "{\r\n        \"createCardInput\": \"{{createCardInput}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "5a9026b453922010c5e2ddeeff7b120a",
    "label": "Freeform card added",
    "name": "VTB#FREEFORM_CARD_ADDED"
  },
  "VTB#SWIMLANE_EXPAND_ALL": {
    "actionDispatch": "VTB#SWIMLANE_EXPAND_ALL",
    "actionPayload": null,
    "actionType": "uxf_client_action",
    "assignmentId": "5ce162f453922010c5e2ddeeff7b1216",
    "label": "Swimlane Expand All",
    "name": "VTB#SWIMLANE_EXPAND_ALL"
  },
  "VTB#SWIMLANE_COLLAPSE_ALL": {
    "actionDispatch": "VTB#SWIMLANE_COLLAPSE_ALL",
    "actionPayload": null,
    "actionType": "uxf_client_action",
    "assignmentId": "661d923453922010c5e2ddeeff7b12d5",
    "label": "Swimlane Collapse All",
    "name": "VTB#SWIMLANE_COLLAPSE_ALL"
  },
  "VTB#ATTACHMENT_UPLOADED": {
    "actionDispatch": "VTB#ATTACHMENT_UPLOADED",
    "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}",
    "actionType": "uxf_client_action",
    "assignmentId": "88fbd6f053922010c5e2ddeeff7b12d2",
    "label": "Attachment Uploaded",
    "name": "VTB#ATTACHMENT_UPLOADED"
  },
  "VTB#DATA_DRIVEN_CARD_ADDED": {
    "actionDispatch": "VTB#DATA_DRIVEN_CARD_ADDED",
    "actionPayload": "{\r\n        \"boardId\": \"{{boardId}}\",\r\n        \"taskId\": \"{{taskId}}\",\r\n        \"swimlaneId\": \"{{swimlaneId}}\",\r\n        \"position\": \"{{position}}\",\r\n        \"laneId\": \"{{laneId}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "9820e67453922010c5e2ddeeff7b1208",
    "label": "Data Driven Card Added",
    "name": "VTB#DATA_DRIVEN_CARD_ADDED"
  },
  "VTB#TOGGLE_SWIMLANE_BODY": {
    "actionDispatch": "VTB#TOGGLE_SWIMLANE_BODY",
    "actionPayload": null,
    "actionType": "uxf_client_action",
    "assignmentId": "b202a2f453922010c5e2ddeeff7b1260",
    "label": "Swimlane Body Toggle",
    "name": "VTB#TOGGLE_SWIMLANE_BODY"
  },
  "NOW_VISUAL_BOARD#SWIMLANE_MOVED": {
    "actionDispatch": "VTB#SWIMLANE_MOVED",
    "actionPayload": "{\r\n        \"id\": \"{{id}}\",\r\n        \"fromPosition\": \"{{fromPosition}}\",\r\n        \"toPosition\": \"{{toPosition}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "c33a16b053922010c5e2ddeeff7b1217",
    "label": "Swimlane Moved",
    "name": "VTB#SWIMLANE_MOVED"
  },
  "VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED": {
    "actionDispatch": "VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED",
    "actionPayload": null,
    "actionType": "uxf_client_action",
    "assignmentId": "decc123453922010c5e2ddeeff7b1243",
    "label": "Freeform placeholder card removed",
    "name": "VTB#FREEFORM_PLACEHOLDER_CARD_REMOVED"
  },
  "VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED": {
    "actionDispatch": "VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED",
    "actionPayload": null,
    "actionType": "uxf_client_action",
    "assignmentId": "dee992b053922010c5e2ddeeff7b12c3",
    "label": "Data Driven Card Form Modal Closed",
    "name": "VTB#DATA_DRIVEN_CARD_FORM_MODAL_CLOSED"
  },
  "NOW_VISUAL_BOARD#CARD_MOVED": {
    "actionDispatch": "VTB#CARD_MOVED",
    "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}",
    "actionType": "uxf_client_action",
    "assignmentId": "7dab16f053922010c5e2ddeeff7b122a",
    "label": "Card Moved",
    "name": "VTB#CARD_MOVED"
  },
  "VTB#WRAPPER_DOM_ACTION_PERFORMED": {
    "actionDispatch": "VTB#WRAPPER_DOM_ACTION_PERFORMED",
    "actionPayload": "{\r\n        \"event\": \"{{event}}\",\r\n        \"name\": \"{{name}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "7a40e516b7122010c5e229a6ee11a937",
    "label": "Wrapper DOM action performed",
    "name": "VTB#WRAPPER_DOM_ACTION_PERFORMED"
  },
  "VTB#SWIMLANE_ACTION_PERFORMED": {
    "actionDispatch": "VTB#SWIMLANE_ACTION_PERFORMED",
    "actionPayload": "{\r\n        \"payload\": \"{{payload}}\",\r\n        \"type\": \"{{type}}\"\r\n}",
    "actionType": "uxf_client_action",
    "assignmentId": "f518127053922010c5e2ddeeff7b125a",
    "label": "Swimlane Action Performed",
    "name": "VTB#SWIMLANE_ACTION_PERFORMED"
  }
}
── more in #developer-tools 4 stories · sorted by recency
── more on @kunal khatri 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/using-the-kanban-boa…] indexed:0 read:17min 2026-08-14 ·