We’ve all been there. You’re building a feature that requires stitching together three different SaaS tools, an internal database, and a legacy REST API that hasn’t been updated since 2018. You start looking at workflow automation tools. On one hand, you have low-code/no-code platforms like Zapier or Make. They’re great for marketing teams, but the moment you need complex data transformation, custom error handling, or version control, you find yourself staring at a wall of limitations. On the other hand, you have heavy-duty enterprise orchestration engines that feel like trying to crack a nut with a sledgehammer.
As developers, we want our integrations to look, feel, and behave like code. We want Git histories, unit tests, CI/CD pipelines, and the ability to handle rate limits and payload transformations without clicking through endless GUI dropdowns.
This week, a project caught my eye on Hacker News that attempts to solve this exact pain point: Ballet. Billing itself as a workflow automation tool that writes integrations against any API, Ballet bridges the gap between low-code ease and developer-grade control. Today, we’re going to dive deep into what Ballet is, how its architecture works, and how you can use it to build robust, code-first integration pipelines.
The Integration Problem: Why Current Solutions Fail Developers
Before we look at Ballet’s code, let’s talk about why integrating APIs is still one of the most frustrating parts of software engineering. Traditionally, we are forced to choose between two paradigms:
- The Visual Workflow Trap: Graphical user interfaces (GUIs) are incredibly fast for simple tasks, but they break down at scale. Merging two visual workflows in Git is impossible. Debugging a silent failure inside a nested conditional block in a web UI is a developer's nightmare.
- The DIY Code Monolith: Writing integrations from scratch in Node.js, Python, or Go gives you ultimate control, but you end up reinventing the wheel. You have to write boilerplate for OAuth2 handshakes, exponential backoff, rate-limiting queues, state persistence, and webhook listener infrastructure.
Ballet aims to be the "middle way." It provides a developer-first framework where workflows are declared in structured code (or lightweight configuration), executed by a robust engine that handles the distributed systems complexity (retries, state, queuing) behind the scenes, and integrates seamlessly with any API—even those without official SDKs or OpenAPI schemas.
How Ballet Works: The Architecture
At its core, Ballet treats integrations as a series of directed acyclic graphs (DAGs) composed of discrete "steps." Unlike traditional workflow engines that require heavy YAML DSLs (Domain Specific Languages), Ballet allows developers to define the flow logic in standard programming languages while it manages the execution state.
Here is a conceptual look at how a Ballet-managed integration flows:
[ Webhook / Event Trigger ]
│
▼
┌──────────────────────────────────────┐
│ Ballet Execution Engine │
│ ┌────────────────────────────────┐ │
│ │ Step 1: Fetch & Validate Data │ │
│ └────────────────┬───────────────┘ │
│ │ (State Managed) │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Step 2: Transform Payload │ │
│ └────────────────┬───────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │Step 3A: Slack│ │Step 3B: DB │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────┘
What makes Ballet unique is its approach to API abstraction. Instead of relying on hardcoded integrations (e.g., a "Stripe Node" or a "HubSpot Node"), Ballet uses a dynamic binding layer. If you can describe the HTTP request or write a small JS/TypeScript snippet to fetch the data, Ballet can turn it into an automated, retriable step in your workflow.
Hands-On: Building Your First Ballet Integration
Let's look at a practical example. Imagine we want to build an automated pipeline that listens for a new user signup on our website (via a webhook), enriches that user's data using a third-party API (like Clearbit or a custom lookup tool), saves the enriched profile to our database, and alerts our sales team on Slack if the user belongs to an enterprise domain.
Here is how we can structure this integration using Ballet’s code-first approach. We'll use TypeScript to define our steps and orchestrate the flow.
Step 1: Defining the Steps
In Ballet, we define our API calls and data transformations as distinct, reusable steps. This keeps our code modular and highly testable.
// steps/enrichUser.ts
import { Step } from '@ballet/sdk';
interface UserSignup {
email: string;
name: string;
}
interface EnrichedData {
company: string;
companySize: number;
role: string;
}
export const enrichUserStep = Step.create<UserSignup, EnrichedData>({
id: 'enrich-user-data',
retryPolicy: {
maxAttempts: 3,
backoffMs: 1000,
},
run: async (input) => {
const response = await fetch(`https://api.enrichment-service.com/v1/lookup?email=${input.email}`, {
headers: {
'Authorization': `Bearer ${process.env.ENRICHMENT_API_KEY}`
}
});
if (!response.ok) {
throw new Error(`Enrichment failed with status: ${response.status}`);
}
const data = await response.json();
return {
company: data.company.name,
companySize: data.company.metrics.employees,
role: data.person.employment.title
};
}
});
Notice what’s happening here. We are writing standard TypeScript. However, because we wrapped it in Ballet’s Step.create, we instantly get automated retries with exponential backoff. If the enrichment API goes down temporarily, Ballet’s engine handles the queueing and retrying without us having to write a single line of redis or bullmq code.
Step 2: Orchestrating the Workflow
Now, let's wire these steps together into a coherent workflow. We will create a flow that accepts the initial webhook payload, runs our enrichment step, conditionally branches based on the company size, and executes parallel actions.
// workflows/signupWorkflow.ts
import { Workflow } from '@ballet/sdk';
import { enrichUserStep } from '../steps/enrichUser';
import { saveToDatabaseStep } from '../steps/saveToDb';
import { sendSlackAlertStep } from '../steps/slackAlert';
export const signupWorkflow = Workflow.create({
id: 'user-signup-enrichment-pipeline',
trigger: 'webhook',
run: async (context) => {
const rawUser = context.payload; // Incoming webhook data
// 1. Run the enrichment step
const enrichedProfile = await context.runStep(enrichUserStep, {
email: rawUser.email,
name: rawUser.name
});
// 2. Save the enriched user to our DB (always runs)
const dbPromise = context.runStep(saveToDatabaseStep, {
email: rawUser.email,
...enrichedProfile
});
// 3. Conditional execution: If it's a hot lead, alert Slack in parallel
if (enrichedProfile.companySize > 500) {
const slackPromise = context.runStep(sendSlackAlertStep, {
message: `🔥 Hot Lead: ${rawUser.name} from ${enrichedProfile.company} (${enrichedProfile.companySize} employees) signed up!`
});
// Run both parallel tasks concurrently
await Promise.all([dbPromise, slackPromise]);
} else {
await dbPromise;
}
}
});
Why the "Code-First" Approach Wins
Looking at the code above, the advantages of Ballet’s paradigm over visual editors or raw scripts become clear:
1. True Local Debugging
Since your workflows are just code, you can run them locally. You don't have to push to a staging environment or trigger a live webhook just to see if your conditional statement works. You can write a Jest or Vitest suite to mock your API responses and assert that the workflow branches correctly.
2. Declarative State & Resiliency
If your server crashes mid-execution on step 3 (Slack alert), Ballet's state tracking knows that step 1 (Enrichment) and step 2 (Database save) have already completed successfully. When the service recovers, it resumes exactly where it left off, preventing duplicate database writes while ensuring the Slack alert is eventually sent.
3. No API SDK Lock-in
Unlike other integration platforms that require you to wait for their engineering team to build an "integration" for a new SaaS tool, Ballet lets you write integrations against any API immediately. As long as you can make an HTTP request, you can turn it into a managed step.
Wrapping Up: Is Ballet Ready for Production?
Ballet represents a major shift in how we should think about workflow automation. It respects the developer's workflow by treating integrations as software rather than visual configuration. It brings the software engineering best practices we love—type safety, git version control, and modular testing—to the chaotic world of API integrations.
If you're tired of fighting with dragging-and-dropping blocks in a web UI, or if you're drowning in boilerplates for your custom-built microservice integration engines, Ballet is absolutely worth spinning up on your local machine.
What do you think? Are you ready to ditch low-code automation tools for a code-first approach, or do you prefer the ease of visual builders for quick tasks? Let me know in the comments below!
Happy coding,
Alex