Running Locally
Development setup, debugging, and best practices for local Floww development.
Development Mode
Start your workflow with auto-reload:
Features:
- Automatic file watching and reload
- Detailed error messages with stack traces
- Hot reload when you change your workflow files
- Real webhook URLs for testing with external services
How floww dev Works
When you run floww dev, the CLI:
- Registers your triggers on the Floww server (webhooks, cron schedules, etc.)
- Routes events to your local machine for execution
- Watches for file changes and hot-reloads your code
This means:
- Your webhooks get real URLs immediately (e.g.,
https://app.usefloww.dev/webhook/w_abc123/custom)
- Events are executed in your local environment with live code changes
- You can test with real external services (GitLab webhooks, cron schedules, etc.)
- All logging happens in your terminal in real-time
Example workflow:
# Start dev server
floww dev
# Your webhook is registered and you get a URL:
# ✓ Webhook registered: https://app.usefloww.dev/webhook/w_abc123/custom
# Send a request to that URL from anywhere:
curl -X POST https://app.usefloww.dev/webhook/w_abc123/custom \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}'
# Event is routed to your local machine and executed
# You see the logs in your terminal immediately
Local-only Testing
For testing without deploying triggers, you can also use localhost:
curl -X POST http://localhost:3000/webhooks/custom \
-H "Content-Type: application/json" \
-d '{"test": true}'
Custom Port and Host
# Custom port
floww dev main.ts --port 8080
# Custom host (accept external connections)
floww dev main.ts --host 0.0.0.0
# Both
floww dev main.ts --port 8080 --host 0.0.0.0
Project Structure
Organize your workflows for maintainability:
my-floww-project/
├── main.ts # Main workflow file
├── workflows/
│ ├── github.ts # GitHub-specific workflows
│ ├── calendar.ts # Calendar workflows
│ └── cleanup.ts # Maintenance tasks
├── utils/
│ ├── notifications.ts # Shared utilities
│ └── validation.ts # Input validation
└── package.json
Modular Workflows
Split complex workflows into modules:
// main.ts
import { getProvider } from "floww";
import { githubWorkflows } from "./workflows/github";
import { calendarWorkflows } from "./workflows/calendar";
const builtin = getProvider("builtin");
export default [
...githubWorkflows,
...calendarWorkflows,
builtin.triggers.onCron({
expression: "0 2 * * *", // 2 AM daily
handler: (ctx, event) => {
ctx.logger.info('Daily cleanup started');
// Cleanup logic
}
})
]
// workflows/github.ts
import { getProvider } from "floww";
const gitlab = getProvider("gitlab");
export const githubWorkflows = [
gitlab.triggers.onPushEvent({
handler: (ctx, event) => {
// Handle push events
}
}),
gitlab.triggers.onMergeRequestEvent({
handler: (ctx, event) => {
// Handle MR events
}
})
];
Provider Configuration
Floww automatically detects which providers you're using. When you run floww dev for the first time with a new provider, you'll be prompted to configure it:
$ floww dev
⚠ Provider "gitlab" with alias "default" not found in namespace
? Would you like to create it? (Y/n)
? GitLab Access Token: **********************
✓ Provider "gitlab:default" configured successfully
Multiple Provider Instances
You can have multiple instances of the same provider using different aliases:
// Personal GitLab account
const gitlabPersonal = getProvider("gitlab", "personal");
// Work GitLab account
const gitlabWork = getProvider("gitlab", "work");
See the Provider Configuration guide for more details.
Environment Configuration
Use environment variables for configuration:
// .env file
API_URL=https://api.example.com
DEBUG=true
// In your workflow
import { getProvider } from "floww";
const builtin = getProvider("builtin");
builtin.triggers.onWebhook({
handler: (ctx, event) => {
const apiUrl = process.env.API_URL;
const debug = process.env.DEBUG === 'true';
if (debug) {
ctx.logger.debug('Processing webhook', { event });
}
// Use apiUrl...
}
})
Debugging
Logging
Use structured logging for better debugging:
import { getProvider } from "floww";
const builtin = getProvider("builtin");
builtin.triggers.onWebhook({
handler: (ctx, event) => {
// Log request details
ctx.logger.info('Webhook received', {
path: event.path,
method: event.method,
userAgent: event.headers['user-agent'],
bodySize: JSON.stringify(event.body).length
});
try {
const result = processData(event.body);
ctx.logger.info('Processing completed', {
result: result.id,
processingTime: Date.now() - start
});
return result;
} catch (error) {
ctx.logger.error('Processing failed', {
error: error.message,
stack: error.stack,
input: event.body
});
throw error;
}
}
})
Testing Webhooks
Use curl or tools like Postman to test your webhooks:
# Test basic webhook (locally)
curl -X POST http://localhost:3000/webhooks/test \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Test with your deployed webhook URL
curl -X POST https://app.usefloww.dev/webhook/w_abc123/test \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Test with headers
curl -X POST http://localhost:3000/webhooks/secure \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{"data": "test"}'
# Test with query parameters
curl -X POST "http://localhost:3000/webhooks/process?userId=123&action=update" \
-H "Content-Type: application/json" \
-d '{"name": "John"}'
Testing Cron Jobs
For testing cron triggers, use short intervals during development:
import { getProvider } from "floww";
const builtin = getProvider("builtin");
// Development - every 10 seconds
builtin.triggers.onCron({
expression: "*/10 * * * * *",
handler: (ctx, event) => {
if (process.env.DEBUG === 'true') {
ctx.logger.debug('Cron test trigger');
}
// Your logic here
}
})
// Production - every hour
// Change to: "0 0 * * *"
Error Handling
Graceful Error Handling
import { getProvider } from "floww";
const builtin = getProvider("builtin");
builtin.triggers.onWebhook({
handler: async (ctx, event) => {
try {
// Main processing logic
const result = await processRequest(event.body);
return { success: true, data: result };
} catch (error) {
// Log the error with context
ctx.logger.error('Request processing failed', {
error: error.message,
requestId: event.headers['x-request-id'],
userId: event.body?.userId
});
// Return appropriate error response
if (error.code === 'VALIDATION_ERROR') {
throw new WebhookError(400, 'Invalid request data');
}
if (error.code === 'NOT_FOUND') {
throw new WebhookError(404, 'Resource not found');
}
// Generic server error
throw new WebhookError(500, 'Internal server error');
}
}
})
Retry Logic for Cron Jobs
import { getProvider } from "floww";
const builtin = getProvider("builtin");
builtin.triggers.onCron({
expression: "0 */2 * * *", // Every 2 hours
handler: async (ctx, event) => {
const maxRetries = 3;
let retries = 0;
while (retries < maxRetries) {
try {
await performTask();
ctx.logger.info('Task completed successfully');
break;
} catch (error) {
retries++;
ctx.logger.warn('Task failed, retrying', {
attempt: retries,
maxRetries,
error: error.message
});
if (retries >= maxRetries) {
ctx.logger.error('Task failed after all retries', {
error: error.message
});
// Could send alert here
} else {
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * retries));
}
}
}
}
})
Performance Tips
Lightweight Handlers
Keep handlers fast and lightweight:
import { getProvider } from "floww";
const builtin = getProvider("builtin");
// Good - lightweight handler
builtin.triggers.onWebhook({
handler: async (ctx, event) => {
// Quick validation
if (!event.body.id) {
throw new WebhookError(400, 'Missing ID');
}
// Queue heavy processing
await enqueueJob('process-data', event.body);
return { queued: true };
}
})
// Avoid - heavy processing in handler
builtin.triggers.onWebhook({
handler: async (ctx, event) => {
// This blocks other requests
await heavyDatabaseOperation();
await sendEmailsToAllUsers();
await generateReport();
return { completed: true };
}
})
Use Storage for State
import { getProvider } from "floww";
const builtin = getProvider("builtin");
builtin.triggers.onWebhook({
handler: async (ctx, event) => {
// Store processing state
await ctx.storage.set(`request:${event.body.id}`, {
status: 'processing',
startTime: Date.now()
});
try {
const result = await processData(event.body);
await ctx.storage.set(`request:${event.body.id}`, {
status: 'completed',
result,
completedTime: Date.now()
});
return result;
} catch (error) {
await ctx.storage.set(`request:${event.body.id}`, {
status: 'failed',
error: error.message,
failedTime: Date.now()
});
throw error;
}
}
})
Production Deployment
When ready for production:
# Deploy to Floww cloud
floww deploy
# View logs
floww logs [workflow-id] # Recent logs
floww logs [workflow-id] --follow # Live tail
See the Deployment guide for more details.