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:
- Platform Extensions - Authentication providers, caching, notifications, file storage
- Application qBits - Business modules like OMS, WMS, CRM features
- 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
customerNotetable 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:
mvn archetype:generate \
-DarchetypeGroupId=io.qrun \
-DarchetypeArtifactId=qqq-qbit-archetype \
-DgroupId=com.example.qbits \
-DartifactId=customer-notes-qbit \
-Dversion=1.0-SNAPSHOTNavigate into the project:
cd customer-notes-qbitThe 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:
{
"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:
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:
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:
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:
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:
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:
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:
mvn testStep 9: Package Your qBit
Build the qBit JAR:
mvn clean packageThis 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:
<dependency>
<groupId>com.example.qbits</groupId>
<artifactId>customer-notes-qbit</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>Register in your MetaDataProvider:
@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?
- Create a GitHub repository for your qBit
- Add documentation - README, usage examples, configuration options
- Submit for review at qbits.qrun.io/submit
- 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
- Browse the qBits Catalog for inspiration
- Read about becoming a qBit developer
- Join the Discord community to share your work
- Check out the qqq documentation for advanced features
Happy building!