--- title: "Building Components - BYO SDK" slug: "building-components-byo-sdk" updated: 2026-10-05T15:19:18Z published: 2026-10-05T15:19:18Z canonical: "docs.unqork.io/building-components-byo-sdk" stale: true --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.unqork.io/llms.txt > Use this file to discover all available pages before exploring further. # Building Components - BYO SDK This guide walks through building a complete custom component from scratch using the BYO SDK. The example component is a task list that displays tasks, lets Creators mark them complete or incomplete, dispatches events when tasks change, and updates the submission data. ## Step 1: Scaffold the Project If you haven't installed the SDK yet, see [Installation](/unqorkai/docs/byo-sdk#installation) in the SDK Overview. Once installed, scaffold a new project using the following command: ```bash npx --package=./sdk.tgz create task-list --framework vanilla --ts ``` The command generates the following project structure: ```plaintext task-list/ ├── package.json ├── vite.config.ts ├── tsconfig.json ├── vitest.config.ts ├── src/ │   ├── entry.ts │   └── TaskList/ │       ├── TaskList.ts │       ├── TaskList.schema.ts │       ├── TaskList.test.ts │       ├── TaskList.mocks.ts │       └── TaskList.css ``` ## Step 2: Define the Schema Define your component's properties and events using the SDK schema utilities: ```typescript // src/TaskList/TaskList.schema.ts import { z } from 'zod' import { defineComponent, defineEvent } from '@unqork/byo-sdk/schema' const TaskSchema = z.object({  id: z.string(),  title: z.string(),  completed: z.boolean().default(false), }) const taskListSchema = z.object({  label: z.string().describe('Label').default('Tasks'),  items: z.array(TaskSchema).default([]),  allowAdd: z.boolean().describe('Allow adding tasks').default(true),  allowDelete: z.boolean().describe('Allow deleting tasks').default(true), }) const taskToggledSchema = z.object({  name: z.string().default('taskToggled'),  payload: z.object({    task: TaskSchema,    items: z.array(TaskSchema),  }), }) const taskAddedSchema = z.object({  name: z.string().default('taskAdded'),  payload: z.object({    task: TaskSchema,    items: z.array(TaskSchema),  }), }) const taskDeletedSchema = z.object({  name: z.string().default('taskDeleted'),  payload: z.object({    task: TaskSchema,    items: z.array(TaskSchema),  }), }) export const taskToggledDefinition = defineEvent({  name: 'Task Toggled',  type: 'taskToggled',  description: 'Fired when a task is toggled',  stability: 'STABLE',  schema: taskToggledSchema, }) export const taskAddedDefinition = defineEvent({  name: 'Task Added',  type: 'taskAdded',  description: 'Fired when a task is added',  stability: 'STABLE',  schema: taskAddedSchema, }) export const taskDeletedDefinition = defineEvent({  name: 'Task Deleted',  type: 'taskDeleted',  description: 'Fired when a task is deleted',  stability: 'STABLE',  schema: taskDeletedSchema, }) export const taskListDefinition = defineComponent({  name: 'Task List',  type: 'taskList',  description: 'A task list component',  schema: taskListSchema,  events: [taskToggledDefinition, taskAddedDefinition, taskDeletedDefinition], }) export type Task = z.infer export type TaskListProps = z.infer export type TaskToggledEvent = z.infer export type TaskAddedEvent = z.infer export type TaskDeletedEvent = z.infer ``` The schema does the following: - Defines component properties and their defaults using Zod. - Defines custom events with typed payloads. - Generates TypeScript types for your component. - Drives manifest generation and the Module Builder UI. ## Step 3: Implement the Component Create a Web Component with Shadow DOM and typed event callbacks: ```typescript // src/TaskList/TaskList.ts import styles from './TaskList.css?raw' import {  type Task,  type TaskListProps,  type TaskToggledEvent,  type TaskAddedEvent,  type TaskDeletedEvent, } from './TaskList.schema.js' export class TaskList extends HTMLElement {  private _label: TaskListProps['label'] = 'Tasks'  private _items: Task[] = []  private _allowAdd: TaskListProps['allowAdd'] = true  private _allowDelete: TaskListProps['allowDelete'] = true  // Props set by the platform  set label(value: TaskListProps['label']) {    this._label = value    this._render()  }  get label(): TaskListProps['label'] {    return this._label  }  set items(value: Task[]) {    this._items = value    this._render()  }  set allowAdd(value: TaskListProps['allowAdd']) {    this._allowAdd = value    this._render()  }  set allowDelete(value: TaskListProps['allowDelete']) {    this._allowDelete = value    this._render()  }  set submissionData(value: unknown) {    if (Array.isArray(value)) {      this._items = value as Task[]      this._render()    }  }  // Callbacks set by the platform: ((event: TaskToggledEvent) => void) | null = null: ((event: TaskAddedEvent) => void) | null = null: ((event: TaskDeletedEvent) => void) | null = null: ((value: unknown) => void) | null = null  connectedCallback(): void {    this.attachShadow({ mode: 'open' })    // Apply styles via adoptedStyleSheets    const sheet = new CSSStyleSheet()    sheet.replaceSync(styles)    this.shadowRoot!.adoptedStyleSheets = [sheet]    this._render()  }  private handleToggle(id: string) {    const updated = this._items.map((item) =>      item.id === id ? { ...item, completed: !item.completed } : item    )    const task = updated.find((item) => item.id === id)!    this._items = updated    this._render()    // Fire event    this.onTaskToggled?.({      name: 'taskToggled',      payload: { task, items: updated },    })    this.onSubmissionUpdate?.(updated)  }  private handleAdd() {    const newTask: Task = {      id: crypto.randomUUID(),      title: 'New task',      completed: false,    }    const updated = [...this._items, newTask]    this._items = updated    this._render()    this.onTaskAdded?.({      name: 'taskAdded',      payload: { task: newTask, items: updated },    })    this.onSubmissionUpdate?.(updated)  }  private handleDelete(id: string) {    const task = this._items.find((item) => item.id === id)!    const updated = this._items.filter((item) => item.id !== id)    this._items = updated    this._render()    this.onTaskDeleted?.({      name: 'taskDeleted',      payload: { task, items: updated },    })    this.onSubmissionUpdate?.(updated)  }  private _render(): void {    if (!this.shadowRoot) return    this.shadowRoot.innerHTML = `      
       

${this._label}

               
             ${this._items            .map(              (item) => `            
  •                            ${item.title}              ${                this._allowDelete                  ? ``                  : ''              }            
  •          `            )            .join('')}        
               ${this._allowAdd ? '' : ''}      
   `    this._bindEvents()  }  private _bindEvents(): void {    this.shadowRoot?.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {      checkbox.addEventListener('change', (e) => {        const id = (e.target as HTMLInputElement).dataset.id!        this.handleToggle(id)      })    })    this.shadowRoot?.querySelectorAll('.delete').forEach((button) => {      button.addEventListener('click', (e) => {        const id = (e.target as HTMLButtonElement).dataset.id!        this.handleDelete(id)      })    })    this.shadowRoot?.querySelector('.add')?.addEventListener('click', () => {      this.handleAdd()    })  } } ``` The component uses the following key patterns: - **Shadow DOM**: Encapsulates styles and prevents CSS leakage. - **Typed callbacks**: Ensure type safety for event payloads. - **Event objects**: Include `name` and `payload` fields matching the event schema. - **Property setters**: Trigger re-renders. - **adoptedStyleSheets**: Applies CSS without re-injecting on each render. ## Step 4: Export the Component Register the component in the bundle entry point: ```typescript // --------------------------------------------------------------------------- // BYOC Entry — vanilla web component (TypeScript) // // Runtime exports: { view, model } — consumed by the UnqorkAI platform. // Schema re-exports: { taskListDefinition, taskToggledDefinition, ... } — consumed by //   byoc build's manifest generator to produce manifest.json automatically. // --------------------------------------------------------------------------- // src/entry.ts import { TaskList as TaskListView } from './TaskList/TaskList.js' import {  taskListDefinition,  taskToggledDefinition,  taskAddedDefinition,  taskDeletedDefinition, } from './TaskList/TaskList.schema.js' // --------------------------------------------------------------------------- // Component: taskList // --------------------------------------------------------------------------- export const taskList = {  view: async () => TaskListView as CustomElementConstructor,  model: async () => taskListDefinition.ModelClass, } // --------------------------------------------------------------------------- // Schema re-exports — manifest generator traces _kind discriminators here // --------------------------------------------------------------------------- export { taskListDefinition, taskToggledDefinition, taskAddedDefinition, taskDeletedDefinition } ``` The platform expects the following exports: - `view` — returns the Web Component constructor. - `model` — returns the generated `ModelClass` from `defineComponent`. - Schema exports — enable automatic manifest generation. ## Step 5: Create Mock Scenarios Define test scenarios for the dev harness: ```typescript // src/TaskList/TaskList.mocks.ts import { defineMockScenario } from '@unqork/byo-sdk/testing' export const defaultScenario = defineMockScenario({  name: 'Default',  description: 'Empty task list',  props: {    label: 'My Tasks',    items: [],  }, }) export const withTasksScenario = defineMockScenario({  name: 'With Tasks',  description: 'Task list with some items',  props: {    label: 'Project Tasks',    items: [      { id: '1', title: 'Design mockups', completed: true },      { id: '2', title: 'Implement component', completed: false },      { id: '3', title: 'Write tests', completed: false },    ],  }, }) export const readOnlyScenario = defineMockScenario({  name: 'Read',  description: 'Task list with add/delete disabled',  props: {    label: 'Completed Tasks',    allowAdd: false,    allowDelete: false,    items: [      { id: '1', title: 'Design mockups', completed: true },      { id: '2', title: 'Implement component', completed: true },    ],  }, }) ``` ## Step 6: Test in the Dev Harness Start the dev server: ```bash npm run dev ``` Open `http://localhost:5173`. The dev harness includes the following features: - Shows your component. - Provides a scenario drop-down. - Logs callback invocations to the console. - Updates in real time as you edit code. Test the following interactions: - Switching between scenarios. - Toggling tasks and checking the console for `onTaskToggled` calls. - Adding and deleting tasks. - Modifying properties in `TaskList.mocks.ts`. ## Step 7: Write Tests Create unit tests using Vitest: ```typescript // src/TaskList/TaskList.test.ts import { describe, it, expect, beforeAll } from 'vitest' import * as exports from '../entry.js' import type { Task } from './TaskList.schema.js' describe('TaskList', () => {  let element: HTMLElement  beforeAll(async () => {    const ViewCtor = await exports.taskList.view()    const tagName = 'test-task-list'    if (!customElements.get(tagName)) {      customElements.define(tagName, ViewCtor)    }    element = document.createElement(tagName)    document.body.appendChild(element)  })  it('renders with default label', () => {    expect(element.shadowRoot?.innerHTML).toContain('Tasks')  })  it('updates when label prop changes', () => {    ;(element as any).label = 'My Tasks'    expect(element.shadowRoot?.innerHTML).toContain('My Tasks')  })  it('renders items', () => {    const items: Task[] = [      { id: '1', title: 'Task 1', completed: false },      { id: '2', title: 'Task 2', completed: true },    ]    ;(element as any).items = items    const taskElements = element.shadowRoot?.querySelectorAll('.task')    expect(taskElements).toHaveLength(2)    expect(taskElements?.[0].textContent).toContain('Task 1')    expect(taskElements?.[1].classList.contains('completed')).toBe(true)  })  it('fires when checkbox clicked', () => {    const items: Task[] = [{ id: '1', title: 'Task 1', completed: false }]    ;(element as any).items = items    let eventFired = false    ;(element as any).onTaskToggled = (event: any) => {      eventFired = true      expect(event.payload.task.completed).toBe(true)    }    const checkbox = element.shadowRoot?.querySelector('input[type="checkbox"]') as HTMLInputElement    checkbox.click()    expect(eventFired).toBe(true)  })  it('hides add button when allowAdd is false', () => {    ;(element as any).allowAdd = false    expect(element.shadowRoot?.querySelector('.add')).toBeNull()  }) }) ``` To run all tests, use the following command: ```bash npm test ``` ## Step 8: Add Styling Create the stylesheet that was imported in Step 3: ```css /* src/TaskList/TaskList.css */ .task-list-root {  font-family: system-ui, sans-serif;  padding: 1rem;  border: 1px solid #e0e0e0;  border-radius: 8px; } .task-list-root h3 {  margin: 0 0 1rem 0;  font-size: 1.25rem; } .tasks {  list-style: none;  padding: 0;  margin: 0 0 1rem 0; } .task {  display: flex;  align-items: center;  gap: 0.5rem;  padding: 0.5rem;  border-bottom: 1px solid #f0f0f0; } .task.completed span {  text-decoration: line-through;  color: #999; } .task input[type="checkbox"] {  cursor: pointer; } .task span {  flex: 1; } .task .delete {  padding: 0.25rem 0.5rem;  background: #f44336;  color: white;  border: none;  border-radius: 4px;  cursor: pointer; } .task .delete:hover {  background: #d32f2f; } .add {  padding: 0.5rem 1rem;  background: #2196f3;  color: white;  border: none;  border-radius: 4px;  cursor: pointer; } .add:hover {  background: #1976d2; } ``` The CSS is imported as `?raw` and applied through Shadow DOM's `adoptedStyleSheets` API, as shown in Step 3. This approach does the following: - Encapsulates styles in the component. - Prevents CSS leakage to the rest of the page. - Applies styles without re-injecting on each render. ## Step 9: Build and Validate Build for production: ```bash npm run build:prod ``` The command runs the following steps: 1. Bumps version to `1.0.1`. 2. Bundles code with Vite. 3. Generates `dist/manifest.json`. 4. Creates `dist/task-list.tar.gz`. > [!NOTE] > Use `npm run build` for development builds without version bumping. ## Step 10: Preview the Production Build Run the following command to open the dev harness serving the production bundle from `dist/`: ```bash npm run preview ``` Before uploading, verify the following: - All functionality works as expected. - No console errors are present. - File size is acceptable. ## Step 11: Use in the Module Builder After uploading `task-list.tar.gz` to Unqork: 1. **Add the component to your module**: - Open the Module Builder. - Locate your custom component in the component tray under the Custom or BYO section. - Drag and drop the Task List component the canvas. 2. **Configure the component settings**: Use the Settings Panel to configure your component's properties: - **Label**: Set to "Project Tasks". - **Items**: Bind to a data source using `={{components.dataSource.value}}`. - **Allow Add**: Toggle to enable or disable adding tasks. - **Allow Delete**: Toggle to enable or disable deleting tasks. 3. **Configure triggers**: In the Settings Panel, configure what happens when events fire: - **onTaskToggled**: Set up a trigger that executes when a task is toggled, like updating task status in a database. - **onTaskAdded**: Set up a trigger that executes when a new task is added, like saving the new task. - For each trigger, specify the context field to store event data and the target component to execute. ## Advanced Patterns ### Lazy Loading Large Dependencies Defer loading heavy libraries until needed: ```typescript export class TaskList extends HTMLElement {  private chartLib: any = null    async loadChartLibrary() {    if (!this.chartLib) {      this.chartLib = await import('./chart-library.js')    }    return this.chartLib  }    async showChart() {    const lib = await this.loadChartLibrary()    lib.render(this.querySelector('.chart-container'))  } } ``` ### Multiple Components in Asset To bundle multiple components in a single asset, export them all from the same `entry.ts` file: ```typescript // src/entry.ts import { TaskList as TaskListView } from './TaskList/TaskList.js' import { taskListDefinition, taskToggledDefinition, taskAddedDefinition, taskDeletedDefinition } from './TaskList/TaskList.schema.js' import { TaskCard as TaskCardView } from './TaskCard/TaskCard.js' import { taskCardDefinition, taskCardClickedDefinition } from './TaskCard/TaskCard.schema.js' // --------------------------------------------------------------------------- // Component: taskList // --------------------------------------------------------------------------- export const taskList = {  view: async () => TaskListView as CustomElementConstructor,  model: async () => taskListDefinition.ModelClass, } // --------------------------------------------------------------------------- // Component: taskCard // --------------------------------------------------------------------------- export const taskCard = {  view: async () => TaskCardView as CustomElementConstructor,  model: async () => taskCardDefinition.ModelClass, } // --------------------------------------------------------------------------- // Schema re-exports — manifest generator traces _kind discriminators here // --------------------------------------------------------------------------- export {  taskListDefinition,  taskToggledDefinition,  taskAddedDefinition,  taskDeletedDefinition,  taskCardDefinition,  taskCardClickedDefinition, } ``` Each component in the asset is independently configurable in the Module Builder. ### Commands for Imperative Actions Commands let the platform imperative actions on your component. Events flow from your component to the platform; commands flow from the platform to your component. #### Define a Command ```typescript // src/TaskList/TaskList.schema.ts import { defineCommand } from '@unqork/byo-sdk/schema' const resetSchema = z.object({  clearCompleted: z.boolean().optional().default(false), }) export const resetDefinition = defineCommand({  name: 'Reset Tasks',  type: 'resetTasks',  description: 'Reset task list to initial state',  schema: resetSchema, }) export const taskListDefinition = defineComponent({  name: 'Task List',  type: 'taskList',  schema: taskListSchema,  events: [taskToggledDefinition, taskAddedDefinition, taskDeletedDefinition],  commands: [resetDefinition], // ← Add commands array }) ``` #### Listen for Commands ```typescript // src/TaskList/TaskList.ts export class TaskList extends HTMLElement {  connectedCallback() {    this.addEventListener('taskList:resetTasks', (e: CustomEvent) => {      const { clearCompleted = false } = e.detail || {}            if (clearCompleted) {        this._items = this._items.filter(task => !task.completed)      } else {        this._items = []      }            this.onSubmissionUpdate?.(this._items)      this._render()    })  } } ``` #### Trigger from the Module Builder Configure a **BYOC Execute** output on a logic component like an Initializer, Decision, or Logic Block: ```json {  "outputs": [    {      "targetId": "taskList1",      "type": "byocExecute",      "value": {        "componentType": "taskList",        "commandType": "resetTasks",        "args": { "clearCompleted": true }      }    }  ] } ``` **When to use commands**: - **Imperative operations**: Reset, refresh, focus, validate, and so on. - **Headless or service components**: Payment gateways, authentication, analytics, and so on. - **Third-party libraries with method-based APIs**: D3, Stripe, Leaflet, and so on. **Key differences from events**: - **Direction**: Commands → component; events → platform. - **Pattern**: Fire-and-forget with no return value; events trigger workflows. - **Use case**: Imperative actions; events represent state changes. ## Next Steps - [CLI Reference](/unqorkai/docs/cli-reference-byo-sdk) — complete command documentation. - [Testing Guide](/unqorkai/docs/testing-guide-byo-sdk) — advanced testing patterns. - [Migration Guide](/unqorkai/docs/vega-to-centauri-byo-migration-guide) — migrate from Vega BYO. **Complete Source Code** The complete task list component source is available in the SDK examples: ```bash npx --package=./sdk.tgz create task-list-example --template examples/task-list ``` --- **Changelog** | Date | Change | | --- | --- | | 2026-09-29 | Accuracy fix: updated command event listener to use namespaced format (taskList:resetTasks); editorial review: removed extra HR separator (EN-8153). | Also known as Unqork Users, or Designer Users; is anyone who is inside the Unqork platform. Schema is a declarative language that defines the structure, content, and constraints of the data. The UI (user interface) is a device's end-user interaction and communication point. It can include display screens, keyboards, and mouse. It is also how a user interacts with your application. The Shadow DOM is a web standard that allows you to create encapsulated, hidden DOM subtrees for your web components. It provides a way to scope CSS styles and JavaScript behavior within a component, preventing them from leaking out and interfering with the rest of the page. Cascading Style Sheets (CSS) is a style sheet language used for presenting how a HTML or XML document looks to end-users. Rendering is the process of loading components and displaying the final result on the front-end screen. A definition APIs (application programming interfaces) are a set of protocols and definitions developers use to build and integrate application software. APIs act as the connective tissue between products and services. Rendering is the process of loading components and displaying the final result on the front-end screen. To execute or perform a a task. In the Operations builder, Events fire (execute) one, or multiple Operations. Focus indicates the element of the graphical user interface which is currently selected to receive input. Elements with focus sometimes have an accesibility border highlighted around them. APIs (application programming interfaces) are a set of protocols and definitions developers use to build and integrate application software. APIs act as the connective tissue between products and services. The Vega (v 2.0.0) runtime is the next-generation engine of the UDesigner platform. Vega uses cutting-edge technologies to improve the Creator and end-user experience.