Build your first habit in 5 minutes (Using Base UI)
This guide walks you through creating your first habit using the Base UI - a visual, no-code interface for building logic and UI. You'll create a simple workflow that analyzes images and calculates their calorie content using OpenAI's vision API.
GUI-First Approach
Base mode provides a visual interface for creating habits (Both Logic and UI) without writing code. Perfect for rapid prototyping and users who prefer GUI tools!
Environment Setup Checklist
Install Node Version Manager (nvm)
- [ ] macOS/Linux: Install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash - [ ] Windows: Use nvm-windows or download Node.js directly from nodejs.org
- [ ] Restart terminal or run:
source ~/.bashrc(or~/.zshrc) - [ ] Verify nvm:
nvm --version
Install Node.js 24
- [ ] Install Node.js 24:
nvm install 24 - [ ] Set as default:
nvm alias default 24 - [ ] Verify Node.js:
node --version(should show v24.x.x)
Video Tutorial
What is Base Mode?
Base is the visual builder for Habits. It provides:
- Visual Workflow Editor - Drag and drop interface for creating habits
- Module Browser - Browse and install available bits (workflow nodes)
- Form Builder - Create custom forms for habit inputs
- Export Options - Package your habits for deployment
- Testing Tools - Test your workflows directly in the browser
Setup Base Locally
Option 1: Using npx (Recommended)
The quickest way to start is using npx:
npx habits@latest baseThis will:
- Download the latest version of Habits
- Start the Base UI server
- Open your browser to
http://localhost:3000/habits/base/
Option 2: Using NPM Global Installation
Install Habits globally for faster startup:
npm install -g habits
# or with pnpm
pnpm add -g habits
# or with yarn
yarn global add habitsThen run:
# If you haven't initialized base bafore, run:
habits init
# Start serving the base:
habits basePrepare Base Directory Checklist
Prepare Base Directory
- [ ] Create a new directory:
mkdir my-habits && cd my-habits - [ ] Init Base in directory:
npx habits@latest init - [ ] Modify
.envas needed (add API keys, etc.) - [ ] Modify
modules.jsonas needed
Run Habits
- [ ] Run Base Mode:
npx habits@latest base - [ ] Open browser at
http://localhost:3000/habits/base/ - [ ] Create your first habit from the UI
Troubleshooting: Port Already in Use Error
- [ ] Run with different port:
--port 8080 - [ ] Or kill process:
lsof -ti:3000 | xargs kill - [ ] Base can also help you kill a port
Creating Your First Habit
Once Base is running, you'll see the main interface with several tabs. Let's create a simple calorie calculator habit.
Step 1: Create a New Habit
- Go to the Habits tab
- Click + New Habit button
- Name the habit
Calculate Calories in Images - Optionally, you can set the description to
Analyze food images and estimates calorie content - Set the Stack Name to the project name, like
Calories Manager
Step 2: Add Nodes to the Workflow
Add OpenAI Vision Node
- In the Node Palette, activate Bits as the node type if not active
- Choose
@ha-bits/bit-openaifrom the module dropdown - Select
Vision Promptas the operation - Configure the node:
- ID:
analysis - Set OpenAI key.
- Params:
- image: Click the small variable picker icon and choose input with value imageBase64, this will translate to
{{habits.input.imageBase64}} - prompt:
You are a knowledgable nutritionist who can guess the amounts of ingredients in the picture in oz, bowls, plates, pieces or in weight. Then know the calories, if you don't know something try to guess it as close as possible. Don't say I don't know.
- image: Click the small variable picker icon and choose input with value imageBase64, this will translate to
- Credentials:
- apiKey:
{{habits.env.OPENAI_API_KEY}}
- apiKey:
- ID:
If you don't see a required bit, piece, or node in the palette, use the Add Module button to install it. This lets you quickly add missing modules directly from the UI before using them in your workflow.
Add Database Storage Node
- Click + Add Node again
- Select Bits as the node type
- Choose
@ha-bits/bit-databasefrom the module dropdown - Select
insertas the operation - Configure the node:
- ID:
store-analysis - Label:
Store Analysis - Params:
- collection:
calories-diary - document:
{{analysis}}
- collection:
- ID:
Step 3: Connect the Nodes
- Click and drag from the output port of the
analysisnode - Connect it to the input port of the
store-analysisnode - The edge will be created automatically
Step 4: Define Habit Output
- Click the small output icon in the habit panel.
- Add an output field:
- Key:
analysis - Value:
{{analysis}}
- Key:
Generate UI
Go to the UI panel and either create the UI manually or use the AI for that.
Testing Your Habit
Option 1: Using the Built-in Test Form
- In the habit editor, click the Play button
- The test form will appear with input fields
- For
imageBase64, you can either:- Upload an image (will be converted to base64)
- Paste a base64 string directly
- Click Run Test
- View the results in the output panel
Option 2: Using the API
Base automatically generates REST API endpoints for your habits:
curl -X POST http://localhost:3000/habits/base/api/habits/calculate-calories-in-images/execute \
-H "Content-Type: application/json" \
-d '{
"imageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}'Viewing Your Habit as Code
Want to see what you built? Click the < > Code button in the habit editor to view the generated habit YAML, stack YAML and .env:
id: calculate-calories-in-images
name: Calculate Calories in Images
nodes:
- id: analysis
type: bits
data:
framework: bits
source: npm
module: "@ha-bits/bit-openai"
label: OpenAI
operation: vision_prompt
params:
image: "{{habits.input.imageBase64}}"
prompt: "You are a knowledgable nutritionist who can guess the amounts of ingredients in the picture in oz, bowls, plates, pieces or in weight. Then know the calories, if you don't know something try to guess it as close as possible. Don't say I don't know."
credentials:
openai:
apiKey: {{habits.env.OPENAI_API_KEY}}
- id: store-analysis
type: bits
data:
framework: bits
source: npm
module: "@ha-bits/bit-database"
label: Database
operation: insert
params:
collection: calories-diary
document: "{{analysis}}"
edges:
- id: analysis__store-analysis
source: analysis
target: store-analysis
sourceHandle: main
targetHandle: main
output:
analysis: "{{analysis}}"
OPENAI_API_KEY=API_KEY_HEREversion: '1.0'
workflows:
- id: calculate-calories-in-images
path: ./calculate-calories-in-images.yaml
enabled: true
webhookTimeout: 30000
server:
port: 13000
host: 0.0.0.0
frontend: ./frontend
defaults:
webhookTimeout: 30000<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calorie Calculator - Analyze Your Food</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
.animate-pulse-slow {
animation: pulse 2s ease-in-out infinite;
}
.glass-morphism {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.file-input-wrapper input[type=file] {
position: absolute;
left: -9999px;
}
.preview-container {
max-height: 400px;
overflow: hidden;
position: relative;
}
.preview-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body class="min-h-screen bg-gray-900 text-white">
<div class="min-h-screen gradient-bg flex items-center justify-center p-4">
<div class="w-full max-w-2xl">
<!-- Header -->
<div class="text-center mb-8 animate-fadeIn">
<h1 class="text-4xl md:text-5xl font-bold mb-4">Calorie Calculator</h1>
<p class="text-xl text-gray-200">Upload a photo of your food to analyze its nutritional content</p>
</div>
<!-- Main Form Container -->
<div class="glass-morphism rounded-2xl shadow-2xl p-8 animate-fadeIn">
<form id="calorieForm" class="space-y-6">
<!-- File Upload Area -->
<div class="space-y-4">
<label class="block text-lg font-medium text-gray-100">
Food Image
</label>
<!-- Drag and Drop Zone -->
<div id="dropZone" class="border-2 border-dashed border-gray-400 rounded-xl p-8 text-center transition-all duration-300 hover:border-white hover:bg-white/5 cursor-pointer">
<div class="file-input-wrapper">
<input
type="file"
id="imageInput"
accept="image/*"
required
class="hidden"
/>
<div id="uploadPrompt" class="space-y-4">
<svg class="mx-auto h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<div>
<p class="text-lg">Drop your image here, or <span class="text-blue-300 font-semibold">browse</span></p>
<p class="text-sm text-gray-400 mt-1">Supports JPG, PNG, GIF up to 10MB</p>
</div>
</div>
<!-- Image Preview -->
<div id="imagePreview" class="hidden">
<div class="preview-container rounded-lg overflow-hidden">
<img id="previewImg" src="" alt="Preview" />
</div>
<button type="button" id="removeImage" class="mt-4 text-red-400 hover:text-red-300 transition-colors">
Remove image
</button>
</div>
</div>
</div>
<!-- Error Message -->
<p id="imageError" class="hidden text-red-400 text-sm mt-2 animate-fadeIn"></p>
</div>
<!-- Submit Button -->
<button
type="submit"
id="submitBtn"
class="w-full bg-white text-purple-700 font-bold py-4 px-6 rounded-xl hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-white/50 transition-all duration-300 transform hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
Analyze Calories
</button>
</form>
<!-- Loading State -->
<div id="loadingState" class="hidden mt-8 text-center animate-fadeIn">
<div class="inline-flex items-center space-x-3">
<div class="w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin"></div>
<p class="text-lg">Analyzing your food...</p>
</div>
</div>
<!-- Success Result -->
<div id="successResult" class="hidden mt-8 glass-morphism rounded-xl p-6 animate-fadeIn">
<div class="flex items-start space-x-3 mb-4">
<svg class="w-6 h-6 text-green-400 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="flex-1">
<h3 class="text-lg font-semibold mb-2">Analysis Complete</h3>
<div id="analysisContent" class="text-gray-200 space-y-2"></div>
</div>
</div>
<button
id="analyzeAnother"
class="mt-4 w-full bg-white/10 hover:bg-white/20 text-white font-medium py-3 px-4 rounded-lg transition-all duration-300"
>
Analyze Another Image
</button>
</div>
<!-- Error Result -->
<div id="errorResult" class="hidden mt-8 glass-morphism rounded-xl p-6 border-red-500/50 animate-fadeIn">
<div class="flex items-start space-x-3">
<svg class="w-6 h-6 text-red-400 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="text-lg font-semibold mb-1">Analysis Failed</h3>
<p id="errorMessage" class="text-gray-300"></p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// DOM Elements
const form = document.getElementById('calorieForm');
const imageInput = document.getElementById('imageInput');
const dropZone = document.getElementById('dropZone');
const uploadPrompt = document.getElementById('uploadPrompt');
const imagePreview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
const removeImageBtn = document.getElementById('removeImage');
const submitBtn = document.getElementById('submitBtn');
const imageError = document.getElementById('imageError');
const loadingState = document.getElementById('loadingState');
const successResult = document.getElementById('successResult');
const errorResult = document.getElementById('errorResult');
const analysisContent = document.getElementById('analysisContent');
const errorMessage = document.getElementById('errorMessage');
const analyzeAnotherBtn = document.getElementById('analyzeAnother');
let selectedFile = null;
let base64Image = null;
// File handling
function handleFile(file) {
imageError.classList.add('hidden');
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
showImageError('Please select a valid image file');
return;
}
// Validate file size (10MB)
if (file.size > 10 * 1024 * 1024) {
showImageError('Image size must be less than 10MB');
return;
}
selectedFile = file;
// Read and display preview
const reader = new FileReader();
reader.onload = (e) => {
base64Image = e.target.result;
previewImg.src = e.target.result;
uploadPrompt.classList.add('hidden');
imagePreview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
// Show image error
function showImageError(message) {
imageError.textContent = message;
imageError.classList.remove('hidden');
}
// Image input change handler
imageInput.addEventListener('change', (e) => {
handleFile(e.target.files[0]);
});
// Click on drop zone
dropZone.addEventListener('click', () => {
imageInput.click();
});
// Drag and drop handlers
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-white', 'bg-white/10');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-white', 'bg-white/10');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-white', 'bg-white/10');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
// Remove image
removeImageBtn.addEventListener('click', () => {
selectedFile = null;
base64Image = null;
imageInput.value = '';
uploadPrompt.classList.remove('hidden');
imagePreview.classList.add('hidden');
imageError.classList.add('hidden');
});
// Reset form
function resetForm() {
form.reset();
selectedFile = null;
base64Image = null;
uploadPrompt.classList.remove('hidden');
imagePreview.classList.add('hidden');
imageError.classList.add('hidden');
loadingState.classList.add('hidden');
successResult.classList.add('hidden');
errorResult.classList.add('hidden');
submitBtn.disabled = false;
}
// Analyze another button
analyzeAnotherBtn.addEventListener('click', resetForm);
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!base64Image) {
showImageError('Please select an image');
return;
}
// Hide previous results
successResult.classList.add('hidden');
errorResult.classList.add('hidden');
imageError.classList.add('hidden');
// Show loading state
loadingState.classList.remove('hidden');
submitBtn.disabled = true;
try {
// Extract base64 data without the data URL prefix
const base64Data = base64Image.split(',')[1];
const response = await fetch('/api/calculate-calories-in-images', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageBase64: base64Data
})
});
const data = await response.json();
if (response.ok && data.status === 'completed') {
// Show success result
displayAnalysis(data.output.analysis);
loadingState.classList.add('hidden');
successResult.classList.remove('hidden');
} else {
throw new Error('Analysis failed');
}
} catch (error) {
// Show error result
loadingState.classList.add('hidden');
errorMessage.textContent = 'Unable to analyze the image. Please try again with a different image.';
errorResult.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
}
});
// Display analysis results
function displayAnalysis(analysis) {
// Parse and format the analysis text
const lines = analysis.split('\n').filter(line => line.trim());
analysisContent.innerHTML = '';
lines.forEach(line => {
const p = document.createElement('p');
p.className = 'py-1';
// Format the line for better readability
if (line.includes(':')) {
const [label, value] = line.split(':');
p.innerHTML = `<span class="font-medium">${label}:</span> ${value}`;
} else {
p.textContent = line;
}
analysisContent.appendChild(p);
});
}
</script>
</body>
</html>Habits Stack Preparation Checklist
Basic Stack Requirements
- [ ] Stack has a
namefield instack.yaml - [ ] Each habit has a unique
namefield - [ ] Each habit has at least one node
- [ ] All API keys stored in
.envfile (not in habit files) - [ ]
.envis in.gitignoreif you have a version control - [ ]
.env.exampleexists with required variable names
If using Server-Side or Full-Stack Logic
- [ ] All habits have clear
inputsdefined - [ ] All habits have clear
outputsdefined - [ ] UI points to correct backend endpoints
- [ ] CORS configured if frontend/backend on different origins
Troubleshooting: Cannot Find stack.yaml Error
- [ ] Verify
stack.yamlexists in current directory - [ ] Or provide full path:
--config /path/to/stack.yaml
Troubleshooting: Missing Environment Variable Error
- [ ] Verify
.envfile exists - [ ] Check variable names match references in habits (e.g.,
${OPENAI_API_KEY}) - [ ] Ensure
.envis in same directory asstack.yaml
Exporting Your Habit
Once your habit is working, you can export it for deployment:
Export as Single Executable, Docker Container or just download the raw files
- Go to the Export tab
- Select Docker as the export type
- Click Export
- Download the ZIP file
- Extract and run:
unzip stack-docker.zip
cd stack-docker
docker-compose up -dExport as Binary
- Go to the Export tab
- Select Binary as the export type
- Choose your target platform (Windows, macOS, Linux)
- Click Export
- Download the executable
- Run it on your target machine:
./habits-bundle
# or on Windows
habits-bundle.exeExport as Desktop App (Experimental)
- Go to the Export tab
- Select Desktop as the export type
- Choose framework (Tauri recommended) and platform
- Click Export
- Download and install the app on your target platform
Export as Mobile App (Experimental)
- Go to the Export tab
- Select Mobile as the export type
- Choose framework (Tauri recommended) and target (iOS/Android)
- Configure backend URL
- Click Export
- Download APK (Android) or IPA (iOS)
Exporting for Production Checklist
Note: We are deprecating support for
capacitor,cordova, andelectron. Future releases will rely exclusively ontaurifor desktop and mobile exports. Please migrate any existing projects to usetaurifor best support and compatibility.
If exporting Server-Side or Full-Stack (Recommended: Docker)
- [ ] Stack tested locally and working
- [ ] Export via Base UI: Export → Docker
- [ ] Download
{name}-docker.zip - [ ] Unzip and run:
docker-compose up -d
If exporting Server-Side (Alternative: Single Executable)
- [ ] Stack tested locally and working
- [ ] Export via Base UI → Export tab → Binary
- [ ] Download binary for target platform
- [ ] Run executable on target machine
If exporting Desktop App (Experimental)
- [ ] Stack tested locally and working
- [ ] Backend URL configured (where app will connect)
- [ ] Choose framework:
tauri(recommended) orelectron - [ ] Choose platform:
windows,mac,linux, orall - [ ] Check build tools:
curl http://localhost:3000/habits/base/api/export/binary/supportor in UI - [ ] For Tauri: Rust, Cargo installed
- [ ] Export via Base UI → Export tab → Desktop
- [ ] If first time: Download scaffold (buildBinary: false)
- [ ] If ready for binary: Enable buildBinary: true
- [ ] Download and test on target platform
If exporting Mobile App (Experimental)
- [ ] Stack tested locally and working
- [ ] Backend URL configured (must be accessible from mobile device)
- [ ] Choose framework:
tauri(recommended) - [ ] Choose target:
ios,android, orboth - [ ] Check build tools:
curl http://localhost:3000/habits/base/api/export/binary/support- [ ] For Android: Java, Gradle, Android SDK installed
- [ ] For iOS: macOS with Xcode installed (iOS builds only work on macOS)
- [ ] Set environment variables:
- [ ]
ANDROID_HOMEorANDROID_SDK_ROOTfor Android - [ ] Export via Base UI → Export tab → Mobile
- [ ] If first time: Download scaffold (buildBinary: false)
- [ ] If ready for binary: Enable buildBinary: true
- [ ] Download APK (Android) or IPA (iOS)
- [ ] Test on real device or emulator
Troubleshooting: Desktop/Mobile Build Fails
- [ ] Check
mobileordesktopsection for missing tools - [ ] Install missing dependencies
- [ ] Try scaffold export first (buildBinary: false) to verify config
- [ ] Check logs for specific error messages
- [ ] Via API, Run:
curl http://localhost:3000/habits/base/api/export/binary/support
Troubleshooting: iOS Build Fails
- [ ] Verify you're on macOS (iOS builds require macOS)
- [ ] Verify Xcode is installed:
xcodebuild -version - [ ] Open Xcode at least once to accept license agreements
Troubleshooting: Android Build Fails
- [ ] Verify
ANDROID_HOME/ANDROID_SDK_ROOTis set - [ ] Verify Java and Gradle versions are compatible
- [ ] Check compatibility in support endpoint response
- [ ] Install Android SDK build tools if missing
Next Steps
Now that you've built your first habit using Base, here are some things to explore:
Learn More Features
- Variables & Expressions - Learn how to pass data between nodes
- Habit Schema - Full schema reference
- Security Best Practices - Keep your habits secure
Try More Examples
- Email Classification - AI-powered email categorization
- Minimal Blog - Full CMS backend with authentication
- AI Cookbook - Generate recipes from ingredients
Switch to Code-First Approach
Ready to work with code directly? Check out:
- First Habit (Code-First) - Build habits using YAML/JSON
- Creating Habits - Advanced creation techniques
Explore More Modules
Base comes with many pre-built modules (bits). Browse the Modules tab to discover:
- AI/LLM: OpenAI, Anthropic, Cohere, local LLMs
- Communication: Email, SMS, Discord, Slack
- Data: Database, file storage, APIs
- Workflows: Activepieces, n8n nodes
- And many more...
Troubleshooting
Base UI Won't Start
If you see "Port already in use":
# Use a different port
npx habits@latest base --port 8080
# Or kill the process using port 3000
lsof -ti:3000 | xargs killModule Installation Fails
- Check your internet connection
- Try clearing the npm cache:
npm cache clean --force - Check the module name is correct
- Try installing again
Habit Execution Fails
- Check all required environment variables are set
- Verify node connections are correct
- Check the execution logs in the Logs tab
- Test each node individually to isolate the issue
Can't See My Changes
If changes aren't reflected:
- Hard refresh your browser (Cmd+Shift+R / Ctrl+Shift+R)
- Clear browser cache
- Restart the Base server
Getting Help
- Examples Directory - Working example stacks
Pro Tip
You can switch between GUI (Base) and code editing at any time. Changes made in Base are immediately reflected in the underlying YAML files, and vice versa!
