Build Your First qBit

Learn how to create, package, and share a reusable qBit component for the qqq ecosystem

qBits are reusable components that extend qqq applications. Think of them as plugins or modules that add specific functionality - from simple utilities to complete business modules.

In this tutorial, you'll build a Customer Notes qBit that adds note-taking functionality to any qqq application with customer data.

What Are qBits?

qBits come in three types:

  1. Platform Extensions - Authentication providers, caching, notifications, file storage
  2. Application qBits - Business modules like OMS, WMS, CRM features
  3. Data qBits - Database connectors, API integrations, data transformations

Our Customer Notes qBit is an Application qBit - it adds business functionality that works with customer records.

What You'll Build

By the end of this tutorial, you'll have a qBit that provides:

  • A customerNote table for storing notes
  • Automatic linking to any customer table
  • A notes widget for the customer detail view
  • A process to add notes via the API

Prerequisites

Before starting, you should have:

  • Completed the Enterprise App tutorial or have equivalent qqq experience
  • Java 21+ and Maven installed
  • Understanding of qqq metadata concepts

Step 1: Create the qBit Project

qBits are Maven projects with a specific structure. Create yours:

bash
mvn archetype:generate \
  -DarchetypeGroupId=io.qrun \
  -DarchetypeArtifactId=qqq-qbit-archetype \
  -DgroupId=com.example.qbits \
  -DartifactId=customer-notes-qbit \
  -Dversion=1.0-SNAPSHOT

Navigate into the project:

bash
cd customer-notes-qbit

The structure looks like this:

customer-notes-qbit/
├── pom.xml
├── src/main/java/com/example/qbits/customernotes/
│   ├── CustomerNotesQBit.java           # Main qBit class
│   └── metadata/
│       └── CustomerNotesMetaDataProvider.java
├── src/main/resources/
│   └── qbit.json                        # qBit manifest
└── src/test/java/

Step 2: Define the qBit Manifest

Edit src/main/resources/qbit.json:

json
{
  "name": "customer-notes",
  "version": "1.0.0",
  "displayName": "Customer Notes",
  "description": "Add note-taking functionality to customer records",
  "author": "Your Name",
  "category": "application",
  "tags": ["notes", "customers", "crm"],
  "dependencies": [],
  "configuration": {
    "customerTableName": {
      "type": "string",
      "required": true,
      "description": "Name of the customer table to attach notes to"
    },
    "maxNoteLength": {
      "type": "integer",
      "default": 5000,
      "description": "Maximum characters per note"
    }
  }
}

This manifest tells qqq:

  • What your qBit is called and does
  • What configuration it needs
  • How to categorize it in the catalog

Step 3: Create the Note Table

Create src/main/java/com/example/qbits/customernotes/metadata/tables/CustomerNoteTableMetaDataProducer.java:

java
package com.example.qbits.customernotes.metadata.tables;
 
import io.qrun.qqq.backend.core.model.metadata.*;
 
public class CustomerNoteTableMetaDataProducer implements MetaDataProducerInterface<QTableMetaData>
{
   public static final String NAME = "customerNote";
 
   private final String customerTableName;
   private final Integer maxNoteLength;
 
   public CustomerNoteTableMetaDataProducer(String customerTableName, Integer maxNoteLength)
   {
      this.customerTableName = customerTableName;
      this.maxNoteLength = maxNoteLength;
   }
 
   @Override
   public QTableMetaData produce(QInstance qInstance)
   {
      return new QTableMetaData()
         .withName(NAME)
         .withLabel("Customer Note")
         .withPrimaryKeyField("id")
         .withFields(List.of(
            new QFieldMetaData("id", QFieldType.INTEGER)
               .withIsEditable(false),
            new QFieldMetaData("customerId", QFieldType.INTEGER)
               .withIsRequired(true)
               .withLabel("Customer"),
            new QFieldMetaData("noteType", QFieldType.STRING)
               .withPossibleValueSourceName("noteType")
               .withDefaultValue("GENERAL"),
            new QFieldMetaData("subject", QFieldType.STRING)
               .withMaxLength(200),
            new QFieldMetaData("content", QFieldType.TEXT)
               .withIsRequired(true)
               .withMaxLength(maxNoteLength),
            new QFieldMetaData("createdBy", QFieldType.STRING)
               .withIsEditable(false),
            new QFieldMetaData("createDate", QFieldType.DATE_TIME)
               .withIsEditable(false),
            new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)
               .withIsEditable(false)
         ))
         .withAssociation(new Association()
            .withName("customer")
            .withAssociatedTableName(customerTableName)
            .withJoinName("noteToCustomer"));
   }
}

Step 4: Define Note Types

Create src/main/java/com/example/qbits/customernotes/metadata/NoteTypePossibleValueSource.java:

java
package com.example.qbits.customernotes.metadata;
 
import io.qrun.qqq.backend.core.model.metadata.*;
 
public class NoteTypePossibleValueSource implements MetaDataProducerInterface<QPossibleValueSource>
{
   @Override
   public QPossibleValueSource produce(QInstance qInstance)
   {
      return new QPossibleValueSource()
         .withName("noteType")
         .withType(QPossibleValueSourceType.ENUM)
         .withEnumValues(List.of(
            new QPossibleValue("GENERAL", "General"),
            new QPossibleValue("CALL", "Phone Call"),
            new QPossibleValue("EMAIL", "Email"),
            new QPossibleValue("MEETING", "Meeting"),
            new QPossibleValue("SUPPORT", "Support Ticket"),
            new QPossibleValue("INTERNAL", "Internal")
         ));
   }
}

Step 5: Create the Add Note Process

Create src/main/java/com/example/qbits/customernotes/processes/AddNoteProcess.java:

java
package com.example.qbits.customernotes.processes;
 
import io.qrun.qqq.backend.core.actions.processes.*;
import io.qrun.qqq.backend.core.model.actions.*;
 
public class AddNoteProcess implements ProcessInterface
{
   @Override
   public void run(RunProcessInput input, RunProcessOutput output) throws QException
   {
      Integer customerId = input.getValueInteger("customerId");
      String noteType = input.getValueString("noteType");
      String subject = input.getValueString("subject");
      String content = input.getValueString("content");
 
      // Validate
      if (customerId == null)
      {
         throw new QUserFacingException("Customer ID is required");
      }
      if (content == null || content.isBlank())
      {
         throw new QUserFacingException("Note content is required");
      }
 
      // Create the note record
      QRecord note = new QRecord()
         .withValue("customerId", customerId)
         .withValue("noteType", noteType != null ? noteType : "GENERAL")
         .withValue("subject", subject)
         .withValue("content", content)
         .withValue("createdBy", input.getSession().getUser().getFullName())
         .withValue("createDate", Instant.now());
 
      // Insert it
      InsertInput insertInput = new InsertInput()
         .withTableName("customerNote")
         .withRecords(List.of(note));
      InsertOutput insertOutput = new InsertAction().execute(insertInput);
 
      // Return the created note
      output.addRecord(insertOutput.getRecords().get(0));
      output.addValue("message", "Note added successfully");
   }
}

Step 6: Create the Process Metadata

Create src/main/java/com/example/qbits/customernotes/metadata/processes/AddNoteProcessMetaDataProducer.java:

java
package com.example.qbits.customernotes.metadata.processes;
 
import io.qrun.qqq.backend.core.model.metadata.*;
 
public class AddNoteProcessMetaDataProducer implements MetaDataProducerInterface<QProcessMetaData>
{
   @Override
   public QProcessMetaData produce(QInstance qInstance)
   {
      return new QProcessMetaData()
         .withName("addCustomerNote")
         .withLabel("Add Customer Note")
         .withProcessClass(AddNoteProcess.class)
         .withInputFields(List.of(
            new QFieldMetaData("customerId", QFieldType.INTEGER)
               .withIsRequired(true),
            new QFieldMetaData("noteType", QFieldType.STRING)
               .withPossibleValueSourceName("noteType"),
            new QFieldMetaData("subject", QFieldType.STRING),
            new QFieldMetaData("content", QFieldType.TEXT)
               .withIsRequired(true)
         ))
         .withOutputFields(List.of(
            new QFieldMetaData("message", QFieldType.STRING)
         ));
   }
}

Step 7: Wire Up the qBit

Create the main qBit class src/main/java/com/example/qbits/customernotes/CustomerNotesQBit.java:

java
package com.example.qbits.customernotes;
 
import io.qrun.qqq.backend.core.model.metadata.*;
import io.qrun.qqq.backend.core.modules.QBitInterface;
 
public class CustomerNotesQBit implements QBitInterface
{
   private String customerTableName;
   private Integer maxNoteLength = 5000;
 
   @Override
   public void configure(Map<String, Object> config)
   {
      this.customerTableName = (String) config.get("customerTableName");
      if (config.containsKey("maxNoteLength"))
      {
         this.maxNoteLength = (Integer) config.get("maxNoteLength");
      }
   }
 
   @Override
   public void registerMetaData(QInstance qInstance)
   {
      // Register the note types
      qInstance.addPossibleValueSource(
         new NoteTypePossibleValueSource().produce(qInstance));
 
      // Register the customer note table
      qInstance.addTable(
         new CustomerNoteTableMetaDataProducer(customerTableName, maxNoteLength)
            .produce(qInstance));
 
      // Register the add note process
      qInstance.addProcess(
         new AddNoteProcessMetaDataProducer().produce(qInstance));
 
      // Create the join from notes to customers
      qInstance.addJoin(new QJoinMetaData()
         .withName("noteToCustomer")
         .withLeftTable("customerNote")
         .withRightTable(customerTableName)
         .withType(JoinType.MANY_TO_ONE)
         .withJoinOns(List.of(
            new JoinOn("customerId", "id")
         )));
   }
}

Step 8: Write Tests

Create src/test/java/com/example/qbits/customernotes/CustomerNotesQBitTest.java:

java
package com.example.qbits.customernotes;
 
import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.*;
 
class CustomerNotesQBitTest
{
   @Test
   void testQBitRegistersTable()
   {
      QInstance qInstance = new QInstance();
 
      // Add a mock customer table first
      qInstance.addTable(new QTableMetaData()
         .withName("customer")
         .withPrimaryKeyField("id"));
 
      // Configure and register the qBit
      CustomerNotesQBit qbit = new CustomerNotesQBit();
      qbit.configure(Map.of("customerTableName", "customer"));
      qbit.registerMetaData(qInstance);
 
      // Verify the note table was registered
      assertThat(qInstance.getTable("customerNote")).isNotNull();
      assertThat(qInstance.getTable("customerNote").getFields())
         .extracting("name")
         .contains("customerId", "content", "noteType");
   }
 
   @Test
   void testQBitRegistersProcess()
   {
      QInstance qInstance = new QInstance();
      qInstance.addTable(new QTableMetaData()
         .withName("customer")
         .withPrimaryKeyField("id"));
 
      CustomerNotesQBit qbit = new CustomerNotesQBit();
      qbit.configure(Map.of("customerTableName", "customer"));
      qbit.registerMetaData(qInstance);
 
      assertThat(qInstance.getProcess("addCustomerNote")).isNotNull();
   }
}

Run the tests:

bash
mvn test

Step 9: Package Your qBit

Build the qBit JAR:

bash
mvn clean package

This creates target/customer-notes-qbit-1.0-SNAPSHOT.jar.

Step 10: Use Your qBit

In any qqq application, add your qBit:

Add to pom.xml:

xml
<dependency>
   <groupId>com.example.qbits</groupId>
   <artifactId>customer-notes-qbit</artifactId>
   <version>1.0-SNAPSHOT</version>
</dependency>

Register in your MetaDataProvider:

java
@Override
public void defineMetaData(QInstance qInstance)
{
   // Your existing metadata...
 
   // Add the Customer Notes qBit
   CustomerNotesQBit notesQBit = new CustomerNotesQBit();
   notesQBit.configure(Map.of(
      "customerTableName", "customer",
      "maxNoteLength", 10000
   ));
   notesQBit.registerMetaData(qInstance);
}

Now your application has full note-taking functionality for customers.

Publishing to the qBits Catalog

Ready to share your qBit with the community?

  1. Create a GitHub repository for your qBit
  2. Add documentation - README, usage examples, configuration options
  3. Submit for review at qbits.qrun.io/submit
  4. Once approved, your qBit appears in the catalog

You can offer your qBit as:

  • Free - Open source, community contribution
  • Paid - Commercial qBit with revenue sharing

What You Built

You created a complete qBit that:

  • Adds a new table with proper relationships
  • Includes business logic via a process
  • Is fully configurable
  • Has tests
  • Can be packaged and shared

This same pattern works for any functionality you want to make reusable.

Next Steps

Happy building!