Skip to content

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

The quickest way to start is using npx:

bash
npx habits@latest base

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

bash
npm install -g habits
# or with pnpm
pnpm add -g habits
# or with yarn
yarn global add habits

Then run:

bash
# If you haven't initialized base bafore, run:
habits init

# Start serving the base:
habits base

Prepare 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 .env as needed (add API keys, etc.)
  • [ ] Modify modules.json as 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

  1. Go to the Habits tab
  2. Click + New Habit button
  3. Name the habit Calculate Calories in Images
  4. Optionally, you can set the description to Analyze food images and estimates calorie content
  5. Set the Stack Name to the project name, like Calories Manager

Step 2: Add Nodes to the Workflow

Add OpenAI Vision Node

  1. In the Node Palette, activate Bits as the node type if not active
  2. Choose @ha-bits/bit-openai from the module dropdown
  3. Select Vision Prompt as the operation
  4. 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.
    • Credentials:
      • apiKey: {{habits.env.OPENAI_API_KEY}}
Missing a module?

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

  1. Click + Add Node again
  2. Select Bits as the node type
  3. Choose @ha-bits/bit-database from the module dropdown
  4. Select insert as the operation
  5. Configure the node:
    • ID: store-analysis
    • Label: Store Analysis
    • Params:
      • collection: calories-diary
      • document: {{analysis}}

Step 3: Connect the Nodes

  1. Click and drag from the output port of the analysis node
  2. Connect it to the input port of the store-analysis node
  3. The edge will be created automatically

Step 4: Define Habit Output

  1. Click the small output icon in the habit panel.
  2. Add an output field:
    • Key: analysis
    • Value: {{analysis}}

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

  1. In the habit editor, click the Play button
  2. The test form will appear with input fields
  3. For imageBase64, you can either:
    • Upload an image (will be converted to base64)
    • Paste a base64 string directly
  4. Click Run Test
  5. View the results in the output panel

Option 2: Using the API

Base automatically generates REST API endpoints for your habits:

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

yaml
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}}"
example

OPENAI_API_KEY=API_KEY_HERE
yaml
version: '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
html
<!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 name field in stack.yaml
  • [ ] Each habit has a unique name field
  • [ ] Each habit has at least one node
  • [ ] All API keys stored in .env file (not in habit files)
  • [ ] .env is in .gitignore if you have a version control
  • [ ] .env.example exists with required variable names

If using Server-Side or Full-Stack Logic

  • [ ] All habits have clear inputs defined
  • [ ] All habits have clear outputs defined
  • [ ] UI points to correct backend endpoints
  • [ ] CORS configured if frontend/backend on different origins

Troubleshooting: Cannot Find stack.yaml Error

  • [ ] Verify stack.yaml exists in current directory
  • [ ] Or provide full path: --config /path/to/stack.yaml

Troubleshooting: Missing Environment Variable Error

  • [ ] Verify .env file exists
  • [ ] Check variable names match references in habits (e.g., ${OPENAI_API_KEY})
  • [ ] Ensure .env is in same directory as stack.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

  1. Go to the Export tab
  2. Select Docker as the export type
  3. Click Export
  4. Download the ZIP file
  5. Extract and run:
bash
unzip stack-docker.zip
cd stack-docker
docker-compose up -d

Export as Binary

  1. Go to the Export tab
  2. Select Binary as the export type
  3. Choose your target platform (Windows, macOS, Linux)
  4. Click Export
  5. Download the executable
  6. Run it on your target machine:
bash
./habits-bundle
# or on Windows
habits-bundle.exe

Export as Desktop App (Experimental)

  1. Go to the Export tab
  2. Select Desktop as the export type
  3. Choose framework (Tauri recommended) and platform
  4. Click Export
  5. Download and install the app on your target platform

Export as Mobile App (Experimental)

  1. Go to the Export tab
  2. Select Mobile as the export type
  3. Choose framework (Tauri recommended) and target (iOS/Android)
  4. Configure backend URL
  5. Click Export
  6. Download APK (Android) or IPA (iOS)

Exporting for Production Checklist

Note: We are deprecating support for capacitor, cordova, and electron. Future releases will rely exclusively on tauri for desktop and mobile exports. Please migrate any existing projects to use tauri for best support and compatibility.

  • [ ] 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) or electron
  • [ ] Choose platform: windows, mac, linux, or all
  • [ ] Check build tools: curl http://localhost:3000/habits/base/api/export/binary/support or 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, or both
  • [ ] 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_HOME or ANDROID_SDK_ROOT for 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 mobile or desktop section 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_ROOT is 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

Try More Examples

Switch to Code-First Approach

Ready to work with code directly? Check out:

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

bash
# Use a different port
npx habits@latest base --port 8080

# Or kill the process using port 3000
lsof -ti:3000 | xargs kill

Module Installation Fails

  1. Check your internet connection
  2. Try clearing the npm cache: npm cache clean --force
  3. Check the module name is correct
  4. Try installing again

Habit Execution Fails

  1. Check all required environment variables are set
  2. Verify node connections are correct
  3. Check the execution logs in the Logs tab
  4. Test each node individually to isolate the issue

Can't See My Changes

If changes aren't reflected:

  1. Hard refresh your browser (Cmd+Shift+R / Ctrl+Shift+R)
  2. Clear browser cache
  3. Restart the Base server

Getting Help


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!

Released under the Apache 2.0 License.