Blog
Technician scheduling software in 2027: Essential capabilities for modern field service
What should technician scheduling software actually do? Nine essential capabilities, from skill matching and SLA priorities to real-time replanning, explainability, and API integration.
Technician scheduling software assigns field technicians to jobs and plans their days so that the right person, with the right skills, arrives within the promised window without wasting hours on the road.
The best tools do this automatically across hundreds or thousands of technicians, and they keep the plan valid as the day changes.
Most products in this category show a calendar and a map. The difference between them is what happens underneath: whether the software can weigh skills, time windows, SLAs, travel, and working-hour rules together, or whether it leaves those tradeoffs to a dispatcher.
This guide lists nine capabilities that separate modern technician scheduling from a digital whiteboard. It's written for operations leaders comparing tools and for product teams at field service software companies deciding what their own scheduling engine needs.
Why technician scheduling has become harder
Two pressures are pushing field service teams to rethink how they schedule.
- The first is labor. In the US HVAC trade alone, industry estimates put the technician shortage at around 110,000 open positions. When every skilled technician counts, a schedule that wastes an hour of their day on avoidable driving is a real capacity problem.
- The second is expectations. Customers expect narrow arrival windows, live updates, and a fix on the first visit. Nobody wants to be anticipating their technician coming in all day.
Both pressures land on the schedule.
Nine essential capabilities
1. Skill and certification matching
The software should know what each job requires and what each technician can do, including certifications, seniority, languages, and equipment access. Hard requirements (a gas-safe certificate) should never be violated. Softer preferences (a technician the customer has worked with before) should be encouraged but not forced.

Look for support for skill levels and customer affinity in addition to simple yes-or-no skills. Matching the right skills is also one of the most direct ways to improve first-time fix rates.
2. Time windows and SLA priorities
Jobs come with customer availability windows, and many come with contractual deadlines.

The software needs to treat a critical SLA differently from a routine visit that can move to tomorrow. That means supporting multiple time windows per job, priority levels, and deadlines that can push lower-priority work out of the plan when capacity runs short.
3. Travel-aware planning with real road data
A schedule that assumes straight-line distances will fall apart on the road. Technician scheduling software should use real travel times, ideally with support for traffic patterns and your own map provider if you already have one.

It should let you choose what to minimize, total travel time or total distance, because the two goals give different answers.
4. Multi-technician visits and job dependencies
Some jobs need two or more people on site at the same time for safety or lifting. Others must happen in order, sometimes with a delay: isolate the power before rewiring, or let a surface dry before painting.

Many tools can't express these rules, so dispatchers track them by hand. If your operation has them, this capability is non-negotiable.
5. Working hours, breaks, and fairness
The plan has to respect shift start and end times, mandatory breaks, maximum working hours, and overtime rules. It should also spread work fairly so the same technicians don't always get the long days or the difficult jobs. Fairness affects retention, and retention matters more when technicians are hard to replace.

Scheduling software can support labor-law-style rules as constraints, but your organization remains responsible for verifying compliance with the regulations that apply to you.
6. Real-time replanning
Every schedule meets reality by mid-morning. A technician calls in sick, a job overruns, or an urgent call comes in. The software should replan quickly while changing as little as possible for everyone else. It should also let dispatchers lock (pin) jobs that are already in progress or confirmed with the customer.

A strong version of this capability also recommends options. When a new urgent job arrives, the dispatcher should see which technicians could take it and what each choice would cost in travel or delayed work.
7. Explainability
Dispatchers and managers won't trust a plan they can't understand. Good technician scheduling software shows why a job was assigned the way it was, which rules were bent, and why some jobs couldn't be scheduled at all.

Look for a breakdown by constraint and clear metrics such as total travel time, overtime, and unassigned jobs.
8. Configurable priorities
Every operation weighs its goals differently. A utility may put SLA compliance far above travel savings, while a residential service company may care most about completing as many jobs as possible per day. The software should let you adjust the relative weight of each goal without rewriting the underlying logic.

9. Integration and scale
Scheduling doesn't live on its own. It needs job data from your FSM or work order system, technician data from HR or workforce tools, and a way to push the finished plan to mobile apps. An API-first design makes this far easier than a closed tool with a separate interface.

Scale matters too. The software should handle your largest region on your busiest day, and it should keep up as you add technicians, regions, or longer planning horizons.
A quick evaluation checklist
Use this table when comparing technicians scheduling software options or reviewing your own engine.
Scheduling software, FSM platform, or optimization engine?
When people search for technician scheduling software, they find three kinds of product.
- FSM platforms with a built-in scheduler cover the full workflow: work orders, mobile apps, invoicing, and scheduling. Their scheduling depth varies widely, from drag-and-drop boards to real optimization.
- Standalone scheduling tools focus on the dispatch board and often add basic auto-assignment. They're quick to adopt but can struggle with complex constraints.
- Optimization engines sit behind a user interface and do the heavy lifting. They don't manage work orders or invoices. Instead, they take in jobs, technicians, and rules, and return an optimized plan through an API. FSM vendors often embed one to strengthen their own scheduling, and large enterprises use one to add optimization to systems they already run.
Where Timefold fits
Timefold is an optimization engine in the third group. The Timefold Field Service Routing model covers the capabilities above: skill and seniority matching, customer time windows, visit dependencies with delays, multi-technician visits, working hours and fairness, and real-time replanning with recommendations. It ships with 50+ pre-configured constraints with adjustable weights, and every plan comes with metrics that show the tradeoffs it made.

It connects through a stateless REST API, so it fits alongside the FSM or work order system you already use. To see how this applies to field teams specifically, read about route optimization for field technicians. If you're preparing to integrate, the data foundations of efficient field service walks through the data you'll need.
FAQ
What does technician scheduling software do?
It assigns jobs to technicians and plans each technician's day, taking skills, time windows, travel, and working hours into account. More advanced tools optimize the whole plan at once and replan automatically when things change.
What's the difference between technician scheduling and dispatching?
Technician scheduling creates the plan ahead of time. Dispatching sends technicians to jobs and manages changes during the day. Modern software blends the two by replanning continuously.
Do I need technician scheduling software if I have an FSM platform?
It depends on how deep your FSM's scheduling goes. If dispatchers still spend hours adjusting assignments by hand, or your FSM can't express rules like dependencies or multi-technician visits, an optimization engine behind your FSM can close the gap.
How many technicians do I need before scheduling software is worth it?
There's no fixed number. Complexity matters more than headcount. A team of 20 with several skill types, tight SLAs, and multi-technician jobs can benefit as much as a team of 200 with simple, uniform work.


Why PlanningAI encompasses scheduling, routing, and strategic decision-making
Origin
The term "Planning AI" emerges naturally from the versatile root word "planning," originally from the Old French plan meaning "ground plan" or "map." Historically, planning implied detailed thinking ahead, encompassing both short-term logistics and long-term strategy.
Over time, especially in multilingual and global business contexts, "planning" has expanded to naturally cover various operational scopes, from immediate scheduling and routing to broader strategic foresight.

Practical
In practice, modern organizational needs blur distinctions between short-term schedules, operational logistics (routing), and strategic decision-making. Consider scheduling: it directly impacts daily efficiency, customer service quality, and staff morale. Routing is a close sibling to scheduling. It integrates geographic constraints, logistical decisions, and real-time adaptability. Together, scheduling and routing form the tactical layer of organizational efficiency.
Strategy
At the strategic layer, PlanningAI helps give insight into larger-scale decision-making challenges, such as resource investment, market entry timing, and capacity planning. The strategic horizon defines parameters within which tactical operations like scheduling and routing are optimized.
Critically, restricting "PlanningAI" solely to long-term strategy ignores this practical, linguistic, and historical reality. Its inherent versatility makes it an intuitive, logical term to encapsulate a spectrum of intelligent decision-making processes.
Intention
Therefore, PlanningAI, as a term, intentionally captures scheduling, routing, and strategy because businesses naturally function across multiple timeframes and complexities.
Recognizing this breadth enables organizations to leverage comprehensive AI-driven solutions for greater agility, foresight, and sustained competitive advantage. Ultimately, embracing Planning AI in its broader meaning aligns closely with how organizations practically operate and how language organically evolves to reflect real-world use cases.
In other words: We believe PlanningAI is the world's best solution to complex, large-scale planning problems.

PlanningAI vs GenAI: What sets them apart?
It seems like you can’t scroll through a tech feed these days without hearing about Generative AI. From ChatGPT writing blog posts, to AI-generated art taking over Instagram, GenAI is everywhere. And just to be clear, we’re hardcore users ourselves. But there’s a misconception we want to clarify. Not every AI falls under the GenAI banner.
Our DevRel Tom Cools already wrote a compelling blog post on the different kinds of AI, and how each kind has different capabilities. However, in this blog post, we’ll explain 6 major differences between PlanningAI and GenAI. Both powerful, both AI, but fundamentally different.

1. Black box vs. explainable
- Generative AI often relies on deep neural networks that are incredibly complex. The model’s inner workings can be difficult to interpret, hence the “black box” label. Even developers can struggle to explain exactly how the system arrived at a certain output.
- Planning AI, on the other hand, uses methods that are more transparent. It’s built on well-established algorithms (like constraint programming, linear optimization, or rule-based systems) that are easier to trace. Users can see how decisions are made, step by step, which is especially important in industries where compliance and explainability are paramount.
[highlight]
In many business applications, stakeholders demand to know why a decision was made. Planning AI delivers that explainability. Generative AI is incredible for creative or less regulated contexts, but in scenarios requiring a clear rationale (think supply chain, workforce scheduling, or compliance-heavy fields), an explainable system wins every time.
[highlight]
2. Non-deterministic vs. deterministic
- Generative AI is non-deterministic. That means it can produce different outputs even when given the same prompt. This variability can be great for creative applications, but it can also lead to inconsistencies.
- Planning AI is deterministic. Give it the same inputs, and you’ll get the same outcome every single time. This consistency is crucial for businesses that rely on predictable results, like production scheduling, logistics planning, or financial forecasting.
[highlight]
Predictability is often the key to operational success. When you need reliable, repeatable results, deterministic models are more appropriate. If you’re aiming for creative inspiration or novel outputs, non-deterministic models shine.
[highlight]
3. New tech vs. proven tech
- Generative AI feels like the “new kid on the block.” While deep learning has been around for a while, the explosion of large language models and image generators is relatively recent. This newness excites people but also means there’s still a lot to figure out: best practices, regulations, ethical frameworks, etc.
- Planning AI has been around for decades. Methods like optimization, constraint programming, and rule-based systems are proven in countless real-world scenarios. They’ve been tested and refined over time, offering reliability that’s hard to beat.
[highlight]
Mature, proven technologies often come with extensive documentation, robust user communities, and well-established integration paths. If your business requires stability and a track record of success, Planning AI can offer peace of mind.
[highlight]
4. Generating new content vs. optimizing what exists
- Generative AI is about creating. Be it text, images, or even code snippets. It’s an amazing fit for brainstorming, content creation, and personalized marketing materials.
- Planning AI focuses on optimizing existing resources and processes. Instead of inventing a new image or paragraph of text, Planning AI schedules your workforce more efficiently, optimizes complex routing problems and helps allocate resources across multiple projects.
[highlight]
If your biggest challenge is to streamline operations, cut costs, or improve your workflow, Planning AI is designed for those optimization tasks. Generative AI can create fresh ideas, but it might not be the best fit for strictly operational goals that need a laser focus on efficiency.
[highlight]
5. Large training datasets vs. manually programmed algorithms
- Generative AI: Creating an LLM requires massive amounts of training data. These large datasets consume enormous computational power (and, yes, electricity) to reach impressive accuracy and capabilities.
- Planning AI uses optimization algorithms that are preprogrammed by experts. While some data may be involved, it doesn’t require training. Instead, Planning AI relies on logical constraints, heuristics, and well-defined optimization strategies.
[highlight]
Collecting, cleaning, and maintaining huge datasets can be expensive and time-consuming. If your business doesn’t have that kind of data. Planning AI can be far more practical to implement and maintain.
[highlight]
6. Environmental impact: “Burns” vs. “Saves”
- Generative AI is computationally heavy. Training large models can consume tons of energy, which raises environmental concerns.
- Planning AI requires less compute power. It’s more about applying targeted, efficient algorithms rather than brute-forcing through billions of data points. As a result, it tends to have a smaller carbon footprint. Additionally, planning AI is used to optimize issues like vehicle routing, which has a massive impact on CO2 emissions.
[highlight]
Sustainability is becoming a core metric for many organizations. While it’s not always a direct apples-to-apples comparison, if your company values a lower environmental impact, Planning AI is often more resource-friendly.
[highlight]
Where does Timefold fit?
We’ll be the first to admit we’re big fans of Generative AI, some of us are even using it to help write code or jump-start content. However, our product is built on PlanningAI principles. It’s explainable, deterministic, and designed to optimize existing processes rather than create new content. This makes it ideal for businesses that need reliable, repeatable, profitable, and environmentally conscious solutions.
Our takeaways
- Generative AI and Planning AI are both “AI,” but they solve very different types of problems.
- Generative AI shines at creating new content and inspiring innovation, whereas Planning AI excels at optimizing complext processes at scale and delivering predictable results.
- Both have unique strengths, but if you need transparency, consistency, and a smaller environmental footprint, Planning AI may be the better fit.

LLMs can't optimize schedules, but AI can
As a cat person, I’m very happy the internet has embraced cats as the “ultimate feel-good” subject when it comes to pictures and videos. And now with LLMs and GenAI the fun is endless! Want a cat sitting on a crocodile while playing the banjo? Boom!

Generated by ChatGPT
Or a cat in a tiny top hat hosting a stand-up comedy show for a crowd of impressed goldfish, here you have it!

Generated by ChatGPT
Of course this is purely for entertainment. LLMs have proven themselves to be very valuable tools that help us brainstorm, proof-read e-mails or even help us rewrite texts for specific audiences. The progress in the past few months has been mind-blowing, which might suggest they're the answer to everything. At least, if you believe the hype machine.
But when it comes to solving practical, real-world problems, they’re not there yet.
LLMs and everyday planning problems
My neighbor Jamie works at a hospital and creates the work schedule for the nurses and doctors. It’s a logical nightmare juggling availabilities, contracts, and other employee preferences. Every week, she spends hours creating the schedule, but there is always somebody who thinks they got the short end of the stick.
Our local plumber, Robert, is always in high demand, constantly driving around the neighborhood. His services are in high demand, but he wastes valuable time on the road because he isn't tackling jobs in the most efficient order. Smarter scheduling would mean helping more customers and spending less time driving.
With all the power that people subscribe to tools like ChatGPT, creating such schedules should be a walk in the park? Right?
Reality is however disappointing: LLMs can’t deal with these tasks very well. Most of it comes down to the sheer size of the problem space: the total number of possible combinations of input parameters. Even a seemingly simple problem like assigning shifts to 10 Employees for a week has a problem size of 1063 combinations. That takes a lot of processing power to solve even with specialized algorithms, let alone trying it with a general language model.
7 days, 3 shifts per day, on average 3 employees per shift, yes that’s a 1 with 63 zero’s behind it!
Sometimes LLM models do find a feasible solution. This is especially true for the newer reasoning models. Those seem to think through the problem and can create a feasible schedule on very small datasets. As these models can reason about discrete steps, they break down the creation of a schedule to smaller steps, just like a human would.

Recent updates have given LLMs to replicate human reasoning.
When you try medium sized datasets, LLMs run both into the problem space issue and in some cases even context window limitations due to the amount of tokens in the input. Although we do believe the latter is a temporary problem as the technology evolves (looking for example at Gemini with a context window of 2 Million Tokens!), the former is going to be much harder to overcome… at least without some help.
A solved problem, almost forgotten
By their very nature, LLMs just predict the next token and “the next token” isn’t what we are looking for when trying to solve planning problems. Luckily, for most gaps in current LLM capabilities, we can look back at “older” forms of AI which resolve these gaps.

The different layers of AI, Timefold fits into the outer layer of old-school AI.
When looking at our problem, we have to peel back the layers of AI evolutions. As mentioned, LLMs and GenAI aren’t the solution here.
Deep Learning and Machine Learning are excellent at pattern recognition and learning from data. Unfortunately, the learning part is often the problem.
ML solutions require large volumes of high quality data to learn from. Real world planning problems usually have their own unique nuances and are often still being done manually… so there is no large amount of high quality data available. No training data, no training, no Machine Learning model.
We need to go even further back, to the origins of AI. In the “old-school” AI layer, we find a pretty unassuming part of AI called Mathematical Optimization. This blob of techniques has been a proven way to solve planning problems for the past 30 years. Here you’ll find techniques such as Linear programming and our personal favorite at Timefold: Metaheuristics.
These algorithms don’t need a big upfront training set to build a model, as the models are programmed manually by domain experts. They work with enormous datasets and find (near) optimal solutions to your planning problems.
It’s frankly a wonder Metaheuristics and friends are so undervalued and get overshadowed by the commoditization of GenAI. By our estimations, 95% of these planning problems remain unresolved while the answer has been out there for decades.
Maybe it got lost in history? It’s time to rebrand this part of our industry and start our own hype machines. It’s time for #PlanningAI.
[highlight]
What is PlanningAI?
PlanningAI is a type of artificial intelligence designed specifically to handle complex planning and scheduling tasks, and to satisfy the constraints of planning problems. It helps you make better decisions by sorting through countless possibilities to find the best solutions. Solutions that save you time, reduce costs, and improve efficiency.
This form of AI can be a bit unwieldy and overwhelming to operationalize. We realized that and removed as many barriers as possible to make it as simple as it can be. We’ve created pre-built models for common use-cases, ready for consumption through a REST API interface on our Platform.
Using these APIs, we can generate a shift schedule for my neighbor Jamie, or calculate an optimized route for our local plumber Robert, without even needing to know anything about solving these complex planning problems. It’s basically our decade long planning expertise in a box.
[highlight]
Not one or the other, but together
While LLMs can’t handle planning problems efficiently, nobody can deny that they’re an amazing enabler. So instead of staying in our respective corners of the AI-Ecosphere, let’s start being a big happy AI family and leverage each other's strengths for the betterment of all!
PlanningAI can leverage Custom GPT models to help domain experts build better PlanningAI models and help explain them to a larger audience. It can also help transform data into information and translate complex ideas into something even the most technophobic person can understand.

LLMs from their part can leverage the capabilities of PlanningAI through “Tool Calling” functionality or the Model Context Protocol, giving LLMs access to advanced algorithms specialized for solving these kinds of problems.

The fusion of AI technologies is reshaping how we solve planning problems, opening up new and innovative solutions. What a time to be tackling these challenges!

How I built an AI company to save my open source project
On this day, 3 years ago, my world fell apart. It was a Thursday. I just finished my second meeting that morning, looked at my inbox and realized it was over. My life’s work was over. OptaPlanner, the popular open source solver we had developed and refined for more than a decade, was going to die. Despite worldwide success on thousands of projects.
I curled into the sofa. Two hours later, my wife found me there, still catatonic.
This is the story of Timefold AI.
This is the story of how I turned a doomed open source project into a fast-growing PlanningAI company.
With commercial success. Without sacrificing our open source values. With a clear goal: free the world from wasteful scheduling.
The hobby project
19 years ago, a colleague showed me his shift rostering project. To make it fast enough, his code had become unreadable and unmaintainable. It wasn’t his fault. The solver software forced his hand. I envisioned a better Solver API design that would prioritize ease-of-use. So in the summer of 2006, I created an open source project to solve planning problems. Nothing serious, just a hobby.
I released it under the Apache License and kept improving it. Over the years, I added more features, examples, documentation and testing. Good engineering practices. Marketing matters too, so I published articles and presented at conferences about it.
It turns out that good documentation is a superpower for open source projects.
By 2012 my hobby project had grown into a software library with deep tech algorithms for vehicle routing problems (VRP), employee rostering, job shop scheduling, and many other Operations Research challenges. Enterprise projects started using it. Even a NASA supplier for satellite scheduling. But it was far from perfect.
Meanwhile, I got married, renovated an old house and struggled to find time for my hobby project. With kids underway, I quickly started running out of spare time to work on it…
You can’t build a world-class product as a hobby.
The day job
By contributing to open source projects, I landed a job at Red Hat, the open source company. I loved working there. By 2013, they wanted to productize my work, so we renamed it to OptaPlanner and Red Hat started selling support for it.
My hobby became my work.
Customers needed help. I honed my talent for modeling enterprise planning problems. I got to work on all kinds of cases, such as routing thousands of field service technicians, scheduling maintenance of airplanes and planning production in factories across the globe. This feedback also shaped my software immensely.
Regularly talking to real users is critical. One real business problem is worth a thousand academic ideas.
The dream
I realized that the world is full of planning problems. Some are automated. Few are optimized.
I still estimate that 95% of companies use a drag-and-drop scheduling UI or mess around in Excel. Great algorithms have existed for decades. But they are too hard to use. I made it my life’s mission to fix that.
Because real-world planning problems are complex. To solve them with ease, my software needed to evolve faster and broader. I needed a team.
You can’t build a world-class product alone.
The engineering team
Luckily the OptaPlanner team grew organically as Red Hat sold support subscriptions. Great minds like Lukas, Radek and Chris joined me. We didn’t always agree immediately, so we worked out a meeting process to fine-tune design proposals, without drama.
In hindsight, alignment was easy: we were all engineers.
Lukas became my right hand. He probably answered more StackOverflow questions than me. As I (slowly) learned to share ownership, to let go, the project started taking big leaps forward. Our solver finally became easy to use.
Meanwhile, users reported ROI (return on investment) numbers that surpassed our wildest dreams…
The gold vein
Most companies don’t expect big gains from planning optimization. They think their manually planned schedules are near-optimal. But they’re wrong. In vehicle routing cases, our solver helps planners to reduce their fleet’s travel time by 25%. For big companies, this saves a hundred million dollars per year. And ten million kg of CO² emissions per year. It gives the world extra time.
So clearly, there’s money to be made with this technology. But our software was 100% open source: Red Hat only sold support. That’s a weak Value Proposition. Especially if there are almost no bugs, support is worthless. I estimate 0.01% of our users paid. Enough to keep the lights on. But not enough to free the world from wasteful scheduling.
Without a proper business model, an open source project doesn’t change the world.
We needed repeatable sales. A great engineering team is not enough. To turn the gold vein into a gold mine, we needed a commercial team focussed on selling to Planning Optimization customers. But that wasn’t the corporate strategy…
You can’t build a world-class product without sales and marketing.
The acquisition
IBM acquired Red Hat in 2019. Initially, everybody got nervous, especially because they owned a competing solver. But after a year, everything seemed normal.
At the end of 2021, I distinctly remember telling my team: “We’re safe. OptaPlanner pays for itself. Red Hat would be crazy to kill our project.” But our project’s revenue was part of a broader product subscription…
The breakdown
By then it was February 2022 - that fateful week three years ago.
On Monday, they told us that the product subscription would move to IBM, without us. It didn’t fit in the strategy.
The first stage is Denial.
On Tuesday, they told the entire company. Every Red Hat sales with an OptaPlanner deal in the pipeline contacted me. They needed a product to sell. But I had no answers.
The second stage is Anger.
On Wednesday, I wanted to rescue the sales pipeline. Months of work. I pleaded with management to move us into another product and to announce it immediately. But it’s not that simple.
The third stage is Bargaining.
On that fateful Thursday, I looked at my inbox and crashed.
The fourth stage is Depression.
Much later, I realized Red Hat made the right decision. A company needs focus. When your company strategy is Hybrid Cloud, your marketing, sales and support teams aren’t experienced (nor incentivized) to deal with scheduling or routing problems too.
The last stage is Acceptance.
The marriage conversation
After my wife found me there, curled up on the couch, I had to get healthy first. When I was back on my feet, I regressed through those stages a few times. It was my life’s work hanging in the balance. But by July 2022 all hope died: it was clear that OptaPlanner had no future.
What if I start my own company around the open source project? Without thinking too much about it, I floated the idea to my wife…
My wife grew up with self-employed parents that worked day and night. She knew the sacrifices to run your own business. She never wanted that for our family. And with two kids and a big mortgage, my salary was needed to pay the bills. So imagine my surprise when - without batting an eyelid - she replied:
“Go for it! We can handle a year without your income.”
No doubt. No resistance. Only optimism. She knows this is my life’s work and she respects that. She mentally even took the leap faster than I did. She’s truly a wonderful woman.
To create a successful startup, you need support from your spouse.
The business model
By then, my reputation as a planning expert preceded me. I could automate any scheduling problem and make it scale (on our solver). So I could start a consulting company and sell my time by the hour. Easy money. But then I would be working on the solver in my spare time again.
“You can’t build a world-class product as a hobby.”
Instead, I had to build a product company. One that offers a subscription on top of the open source project. With a strong value proposition. Support alone wasn’t enough, so I decided to go for the open core business model: keep developing the open source solver, but sell proprietary services on top of it. I couldn’t build all that alone. And software developers don’t work for free.
“You can’t build a world-class product alone.”
Bootstrapping would take too long. I needed an investor. Not an Angel Investor that wants a reliable return, but a VC firm that’s in it for the long run and wants to take an ambitious, big risk. But I didn’t know how to build a startup. Nor fund it.
“You can’t build a world-class product without sales and marketing.”
Luckily, I live in Ghent - a strong startup hub in Europe.
The advice
A solo entrepreneur advised me: “Don’t do it alone”. Purely for mental reasons. You need someone to not just share, but also understand the hardship and complexity. A cofounder. Not an employee, not an adviser, but someone who’s all-in. He was right.
A few dozen people had asked me to start a company before. They all dreamt of riches and success, but none had startup experience. Neither did I.
I started reading books. The Founder’s Dilemma was highly informative. I realized I needed a CEO. Someone with experience on how to run a company. Someone with a very different skillset and different mindset than myself. The science is clear: cofounders need to complement each other. Balance each other out. And challenge each other.
So how could I find an experienced startup CEO?
The investor
A friend of mine put me in contact with Thomas, an investor of Smartfin.
I sent him a convincing email and we met up at a local coffeehouse for a 30 minute meeting. There are rules on how to pitch to an investor. I didn’t know any of them. I followed none and I don’t recommend that. Yet, he kept listening and recognized the value. After talking for two hours, I asked him to connect me to a potential CEO.
It turns out that Smartfin is the biggest seed-stage VC investor in Belgium.
After that meeting, Thomas called Maarten - a serial entrepreneur mastermind - and told him “I’ve just heard an interesting pitch from a technical guy, but this is not investment ready.”
In hindsight, I was clueless on how to build a company.
The cofounder
In September 2022, I met Maarten for the first time. Through some freak coincidence he lives three streets away from me. He’d probably call it destiny.
I wanted a CEO with expertise in sales and administration.
I found a CEO with expertise in venture building, hiring, funding and a bit of sales.
I didn’t get what I wanted, I got what I needed. As a founder, he had built 3 companies from the ground up already. That left wisdom and battle scars.
Because a seasoned founder doesn’t look at a startup idea like normal people. They see the pain. They question the Product Market Fit. They are brutally critical and despise wishful thinking, because without revenue, the startup dies. Ideas are cheap, execution is expensive.
Maarten’s favorite line: “Assumption is the mother of all fuckups.”
In our sparring sessions, we worked out a plan and focussed on the pain points. I didn’t always understand him. Nor did he me. We often still don’t. I haven’t led a company through financial hardship before and he hasn’t seen how technical debt compounds on long-term software like terminal cancer. But we learned to trust each other. To listen to each other, especially when we don’t agree.
After a month, we took the leap.
The leap
Every founder needs to take the leap: exchange predictable income for uncertainty.
For me, that came in October: I resigned from Red Hat and invested part of our family’s savings in the company. The rest of our savings steadily eroded each month. The clock was ticking. Work-life balance went out the window.
Fun fact: 90% of startups fail.
The company
On December 8th, 2022 Maarten and I registered our company. We named it Timefold, because we like to say that our algorithms fold space and time.
Afterwards we got breakfast and Maarten announced to the entire restaurant: “We just got married!” There’s truth in that.
One of our first expenses went to a designer to create an elegant logo:

The AI hype
After ChatGPT came out, every company needed an AI story. Including our potential customers. Investors loved it too. Should we ride the AI wave?
Timefold’s algorithms are categorized as Artificial Intelligence. But its core is not a Large Language Model or even Machine Learning. It doesn’t use training data. It will never evolve into an AGI. But it’s unrivaled at optimizing planning and scheduling.
I’d even argue it’s a superintelligence because the results leapfrog human capabilities:

We decided to capitalize on the buzz and registered timefold.ai. Later we coined the term PlanningAI.
The pre-seed funding
We needed money to hire a team. Maarten got us in front of every notable VC investor in Belgium.
VC firms collect a large pool of money and invest it in startups. They prefer startups with billion dollar potential, to cover losses on startups that go bankrupt. High risk, high reward. They play the long game. They’ve seen first-hand the growth challenges and impact of strategic decisions for dozens of companies at various stages. Their advice is invaluable. So is their network.
Here’s how it works: a VC typically takes a 20-25% stake. The amount of money varies. They don’t mind a 10% ESOP pool for employee shares. Founders might get a wage to avoid personal financial stress, but then also need skin in the game, typically by investing personally. The VC takes a seat on the board, but doesn’t meddle in day-to-day management. They mainly advise on C-suite hires and strategic company direction. It’s a good deal, but they won’t do the work for you.
What about open source? I didn’t want to end up with a bait-and-switch on my hands. Not every VC gets it, but many VCs respect the distinct advantage of open core companies in the developer API market.
Smartfin did. They offered us the most money and a wise, experienced director for the board. So we accepted their term sheet and went through the due diligence period.
At this point, we were basically two guys with an idea. In March 2023 - six months after I resigned - we closed the deal and they gave us a cool 2 million euro.
Afterwards we got lunch and Maarten asked them:
“How many cases do you get per year?”
“About 1200.”
“And how many do you fund?”
“Two. Maybe three.”
Maarten and I looked at each other. Only 0.25%. I still remember the expression on his face: flabbergasted.
The fork
In May, we came out of stealth. We forked OptaPlanner as Timefold Solver and told the world. Soon after, we talked to prospects and by the end of summer we landed our first big customer.
Meanwhile, the original OptaPlanner team joined Timefold. We were back in business!
The Product Market Fit
But sales were rare. We had plenty of inbound - companies using our open source solver in production - but few that saw enough reason to buy. Our Value Proposition was still too weak.
Companies fail because they fail to achieve Product Market Fit.
PMF is almost holy for startups. Maarten obsessed on these two questions:
1) Do people want to use it?
2) Do people want to pay for it?
Most founders fool themselves on these simple questions. I assumed because our solver saves our users a ton of money, they’ll happily pay for it. But assumption is the mother of … Well, I was wrong.
To become viable, we needed a stronger proprietary offering on top of the open source solver. Without crippling the solver.
The platform
The obvious answer was to build a cloud SaaS platform. Almost every other successful open source company did that. But the devil is in the details.
One of our first hires was magic: a next level cloud architect that helped me deliver Employee Shift Scheduling as a docker REST service for an early customer. He started building an efficient architecture to orchestrate solver runs in the cloud. This became the Timefold Platform.
But customers aren’t looking to buy a platform. They’re looking to buy a solution for their problem. Their planning problem. So we created REST APIs for common planning problems - such as Field Service Routing and Employee Shift Scheduling - and deployed them as PlanningAI models on our platform:

As we were building up these teams - Solver, Platform, Models and Solutions - we needed to become a real company.
The startup team
Every aspect of building a startup is a world of expertise on its own. And if you mess up one part, it affects the whole startup. I had many roles and I wasn’t an expert at most of them. Maarten too. So we hired bright leaders who did it better, mostly from the Ghent tech scene.
For me personally, this started one of the hardest periods of my career.
Over the course of a year, one by one, many of my responsibilities were taken away and given to new hires.
I felt like I was losing control.
I wasn’t.
I felt like I could do those jobs better myself.
I couldn’t.
My role was cut up into our:
- Head of Engineering that organizes a steady pace of progress and doesn’t even blink under stress
- Head of Product that tracks everything and deeply understands that a product is more than code
- Head of Customer Success that groks complex constraints and speaks business too
- Head of Marketing that wrote the most crisp copy on what my tech does that I’ve ever seen
Maarten went through this too, but he had gone through this several times.
He delegated responsibilities to our Chief Commercial Officer (CCO) and Chief Financial Officer (CFO).
Smart, experienced people - each and everyone of them - but still our new leadership team went through growing pains. For me personally, alignment felt hard: they were not all engineers. Luckily! Otherwise the company wouldn’t thrive. Or even survive.
With this leadership in place, we’ve handpicked a team of 30+ specialists, each experts in their domains, dedicated to share our planning solutions with the world:

Together, we've built Timefold.
The seed funding
By mid 2024, our cloud product took shape. It was still a beta version, but a few select customers already used our scheduling and routing APIs in production.
Yet, revenue was still low. There was no real sales organization yet. We needed to build the product, before we could sell it. To continue that, we needed extra funding.
There is no middle ground between Bootstrapping and VC funding. Once you take the VC path, it’s natural to raise multiple rounds of funding, such as seed, series A and series B. The ambition is to grow and carve out a path to profitability.
This time, we pitched internationally. As an AI SaaS platform with deep tech, we got a good deal of attention. We ended up signing with Lakestar, one of Europe’s top VC firms. This got us another 6 million euro in the bank and an extra board director who can hustle us an intro to strategic customers and advisors.
The circus
Up to that point, our office was an orangerie, an old greenhouse building in Maarten’s garden. Good enough for a remote-first company. Better than a garage. But between the thorny plants, we ran out of space.
So in September 2024, we moved to the Wintercircus, a huge 130-year old building in the center of Ghent that is now the heart of the startup scene in Belgium.

The SaaS launch
In October 2024 we launched our SaaS platform app.timefold.ai, with REST APIs for Vehicle Routing Problems and Employee Rostering. Soon we’ll add Production Scheduling. And it never crashed. Even now, as enterprise customers hammer it daily to solve big datasets with tens of thousands of employees.
Meanwhile, our solver team made the open source solver remarkably faster, based on feedback from the community and our models team. It’s amazing what “eating your own dog food” can do for a company.
The Go To Market
We also began our outbound marketing campaigns. Meanwhile, our account and solutions teams worked out how to make our customers successful quicker.
Personally, I started posting on my LinkedIn account on how to approach complex scheduling and routing in the real-world. In a just few months, I became a well-known voice in the Operations Research community.
But honestly, we’re still fine-tuning our Go To Market. When it's fully polished, maybe I can write another article about that? It’s good marketing!
The future
This isn’t the entire story of Timefold. This is just my part of the story. Others did far more for Timefold than I could depict here. And our users made the real impact, by applying our technology on thousands of cases.
This is only the beginning.
Our technology is ready.
Our company is thriving.
Our roadmap is ambitious. More on that coming soon.
Our community is geared up. Join us, and subscribe to the newsletter in the footer.
Our APIs are thoroughly documented. Explore it for yourself.
Now is the time to free the world of wasteful scheduling!

Optimizing Santa’s Travels with Timefold Solver
Merry Christmas everyone!
We are getting extremely close to Christmas and Santa’s preparations are in full swing. Santa is running a very serious enterprise!
- The gifts needs to be constructed and nicely packaged
- Elves need a work schedule so they know when and where they should help
- And then there is the big question of “how do I visit everyone as efficiently as possible?”
These planning problems are typically tricky to solve… but we can all do this using the power of Java and Timefold, an Open Source AI Solver. So put on your Christmas hats, open your laptops and let’s see how we can use these tools to optimize Santa’s road trip! (or is that a sky trip,… I don’t know).

Video walkthrough
This blogpost is also available as a step-by-step video.
Step 1: Model our domain
First, we model our domain, just like we’d do with any other application. With Timefold Solver you can model your domain in Plain Java Objects. Our class diagram will look something like this.

The class structure is pretty straightforward. We got the jolly big man on one side and we have the children Santa wants to visit on the other. They both have a link to the Location object, which is nothing more than a POJO containing the latitude and longitude.
Next we need to order the Visits so Santa can be home again as soon as possible. This is where Timefold Solver comes in. The solver will find a near-optimal order of those visits to keep Santa’s travel time to a minimum.
Timefold Solver needs a little bit more information than a plain POJO. Much like the Java Persistence API (JPA), we need to add a few annotations to our classes for the magic to work. In our case we need to add some annotations to our Santa class. The Visit and Location classes remain untouched.
@PlanningEntity
public class Santa {
private Location homeLocation;
@PlanningListVariable
private List<Visit> visits;
// excluded constructor getter/setter
}
@PlanningEntity indicates the solver should make changes to instances of the annotated class. Anything which is changeable is typically called a planning variable. An entity can have multiple planning variables, but in this case we only want the solver to change the list of visits Santa will use. To indicate this, we annotate the visits with @PlanningListVariable.
The solver will first fill the list with visits using a greedy algorithm. Greed isn’t good, especially not during Christmas times, so this initial solution will be far from optimal. To improve the situation, Timefold Solver moves the items in the list, looking for a combination which results in a better solution.
Step 2: Add Constraints
We have however not defined what a “good” or “better” solution is. Next, we define a constraint.
A constraint is a business rule expressed in code which modifies the score for a given solution. In our example, the best solution minimizes total travel time. We model this in Timefold Solver using Constraint Streams.
public class SantaConstraintProvider implements ConstraintProvider {
// NOTE: Normally you'd want to calculate a Distance Matrix before starting the solver.
DrivingTimeCalculator drivingTimeCalculator = DrivingTimeCalculator.getInstance();
public static final String MINIMIZE_TRAVEL_TIME = "minimizeTravelTime";
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[]{
minimizeTravelTime(factory)
};
}
/**
* Creates a constraint which will reduce the SOFT score by 1 for each second driven by a Santa.
*/
Constraint minimizeTravelTime(ConstraintFactory factory) {
return factory.forEach(Santa.class)
.penalizeLong(HardSoftLongScore.ONE_SOFT,
santa -> drivingTimeCalculator.getTotalDrivingTimeSeconds(santa))
.asConstraint(MINIMIZE_TRAVEL_TIME);
}
}
In this example for constraint minimizeTravelTime, we select all Santas (yes, in some situations there might be some helpers) and we calculate the total driving time for each Santa. Every second a Santa needs to drive to deliver the gifts will be penalized, decreasing the score of the solution. The solver uses an advanced algorithm to optimize this score.
Santa is a bit weird when it comes to calculating travel time. He doesn’t need roads, so we calculate the distance “as the crow flies”, using the Haversine formula. To convert the distance to total travel time, we’ll assume Santa’s average speed is half the speed of light. You can find the full implementation of this calculation in the DrivingTimeCalculator class on GitHub. In most cases, you should calculate a distance matrix before starting the solving process.
Step 3: Initialize the problem and run!
Now it’s time to bring it all together. The SantaPlan class is once again a POJO, this time collecting all the elements Timefold Solver can work with. It’s annotated with @PlanningSolution and holds all problem facts, planning entities, and a score.
@PlanningSolution
public class SantaPlan {
@PlanningEntityProperty
private Santa santa;
@ProblemFactCollectionProperty
@ValueRangeProvider
private List<Visit> visits;
@PlanningScore
private HardSoftLongScore score;
// excluded constructor getter/setter
}
This class contains:
- Our Santa as a @PlanningEntityProperty. This indicates that for this solution, Santa is a planning entity, aka an object which will be changed by the solver.
- A list of Visits. These are the visits which will be assigned to Santa. It’s annotated both with @ProblemFactCollectionProperty to indicate it’s a collection of facts which don’t change while solving, and @ValueRangeProvider to indicate this field provides a range of values the solver can use to assign to Santa, specifically to his @PlanningListVariable field shown earlier.
- A score field annotated with @PlanningScore, which the solver will update with the score of the solution after it has evaluated the constraints.
Next we need a Solver instance which will accept this @PlanningSolution and start working its magic (aka math) on it. While Timefold Solver works in any Java application, when using the Spring or Quarkus extensions, a SolverFactory is autoconfigured based on your configuration properties and classes found on the classpath, like the SantaConstraintProvider we showed before.
In this example I’ll use Quarkus, mainly because the logo looks like a star and has the most Christmas-y colors out of the 2 frameworks.
public class SantaResource {
private final SolverFactory<SantaPlan> solverFactory;
// CDI Injected solverFactory
public SantaResource(SolverFactory<SantaPlan> solverFactory) {
this.solverFactory = solverFactory;
}
/**
* Uses a solver to sort a list of visits
* @param visits the locations santa needs to visit
* @return an optimized plan
*/
public SantaPlan solve(List<Visit> visits) {
// This is the location of Santa's village in Rovaniemi, Finland
Santa santa = new Santa(new Location(66.5039, 25.7294));
SantaPlan santaPlan = new SantaPlan(santa, visits);
Solver<SantaPlan> solver = solverFactory.buildSolver();
return solver.solve(santaPlan);
}
}
Without configuration the solver will keep searching for better solutions indefinitely. That’s not good, because Santa’s deadline is coming up. Luckily, we can determine how long the solver should run with some configuration.
quarkus.timefold.solver.termination.spent-limit=5s
Note: The call to Solver.solve() above is blocking. To execute asynchronously, we could inject and use a SolverManager instead.
Step 4: Visualize
We can run the code with the Solver, calculate a near-optimal route and then print it… but printing it just doesn’t work for Santa, he needs a map!
These kinds of problems just beg to be visualized. While Timefold doesn’t come with visualizations out of the box, we can expose the data through an API and then have some Javascript to visualize it. In this case, I’ll use the popular LeafletJS library together with the data from OpenStreetMap to visualize our route.
@Path("santa")
public class SantaResource {
// Fields and constructor excluded
@POST
@Path("plan")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public SantaPlan solve(List<Visit> visits) {
Santa santa = new Santa(new Location(66.5039, 25.7294));
SantaPlan santaPlan = new SantaPlan(santa, visits);
Solver<SantaPlan> solver = solverFactory.buildSolver();
return solver.solve(santaPlan);
}
}
The code above shows the necessary annotation to expose the objects through an API using Quarkus. Next is the Javascript code used to create a clickable map and render the results from the solver.
// Prepare a custom icon, since default one is not "Christmas" enough.
const treeIcon = L.icon({iconUrl: 'tree-marker.png', iconSize: [20, 20]})
const map = L.map('map', {doubleClickZoom: false}).setView([66.5039, 25.7294], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
}).addTo(map);
// Create a layer group,
const linesGroup = L.layerGroup().addTo(map);
const visitsOnMap = [];
// Allow adding visits on click
map.on('click', function(e) {
const { lat, lng } = e.latlng;
addLocation(lat, lng);
});
// Add a location to the map and to the javascript array
function addLocation(lat, lng) {
visitsOnMap.push({
id: visitsOnMap.length,
location: {
lat: lat,
lng: lng
}
})
// Add a marker at the clicked location
L.marker([lat, lng], {icon: treeIcon}).addTo(map);
}
// The function which will call the solver and visualize the result
function solve() {
linesGroup.clearLayers();
fetch('http://localhost:8080/santa/plan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(visitsOnMap)
})
.then(response => response.json())
.then(data => {
const homeLocation = data.santa.home;
const locations = data.santa.visits.map(visit => visit.location);
L.polyline([homeLocation, ...locations, homeLocation]).addTo(linesGroup);
return data;
})
.catch(error => {
console.error('Fetch error:', error);
});
}
The result is a backend and a UI, with a map which allows us to add locations and a "solve" button which will call the solver and eventually draws the result on the map.
The full implementation of this very basic UI can be found in the /src/main/resources/ folder of the GitHub repository.
Sending off Santa
In this post we helped Santa to find a near-optimal route. This is only a basic example of what is possible in Java using Timefold Solver. Finally, some inspiration for people who’d like to make changes:
- Use a SolverManager instead of the SolverFactory and run the solver asynchronously while updating the UI with intermediate results
- Add multiple helpers for Santa, splitting the visits among them
- Add more constraints, for example forcing Santa to only visit kids when they are asleep
You can find this project on GitHub. Be sure to check it out and try the demo yourself! For more elaborate examples, check out the extensive Timefold Solver Quickstarts repository.
To all who celebrate I wish you a very merry Christmas and all the good things in the new year!

Java versus Python performance benchmarks on PlanningAI models
Performance is a top priority for Timefold Solver, since any performance gains means we can find the same solution in a less amount of time (or alternatively, a better solution in the same amount of time). This is also true for Python, which is typically one or two orders of magnitudes slower than Java. Through a variety of techniques, we are able to make Python reach 25% of Java’s throughput. Read on to find out the methods we deploy to make Python’s performance better, and see how Timefold Solver for Python performance compares to Java on a variety of PlanningAI models.
[highlight]
Note: Usage of Python in this article refers exclusively to the CPython implementation.
[highlight]
Why is Python relatively slow?
Python’s performance challenges are mostly due to design decisions:
Global Interpreter Lock
The Global Interpreter Lock, or GIL for short, is a lock that prevents multiple threads from executing Python code at the same time. This is because Python’s core data structures, such as dict, list and object are not thread-safe.
When the GIL was introduced (Python 1.5, in the year 1998), multi-threaded programs were rare (most computers only supported one thread at the time), and as such, using a GIL simplifies Python extension developer experience and does not degrade single-threaded perfrmance. The GIL was a good idea for its time, but gradually became worse as multi-threaded programs and computers became more common. Currently, Python s working on removing the GIL, but it will be several releases before it is widely available.
Dynamically Typed
Python is dynamically typed, which means the result type of operations cannot be determined before their execution. Moreover, many operations such as addition and attribute lookup are also dynamic, since they look up attributes on their operands' types. As a result, this seemly simple python code:
def add(x, y):
return x + y
Does something a lot more complex underneath:
def ignore_type_error_if_builtin_type(arg_type, method, x, y):
try:
return method(x, y)
except TypeError:
if arg_type in [int, str, list, ...]:
return NotImplemented
raise
def add(x, y):
x_type = type(x)
x_add = x_type.__dict__.get('__add__')
y_type = type(y)
y_radd = y_type.__dict__.get('__radd__')
if x_add is None and y_radd is None:
raise TypeError('...')
elif y_radd is None:
out = ignore_type_error_if_builtin_type(x_type, x_add, x, y)
if out is NotImplemented:
raise TypeError('...')
return out
elif x_add is None:
out = ignore_type_error_if_builtin_type(y_type, y_radd, y, x)
if out is NotImplemented:
raise TypeError('...')
return out
else:
if x_type != y_type and issubclass(y_type, x_type):
out = ignore_type_error_if_builtin_type(y_type, y_radd, y, x)
if out is NotImplemented:
out = ignore_type_error_if_builtin_type(x_type, x_add, x, y)
if out is NotImplemented:
raise TypeError('...')
else:
out = ignore_type_error_if_builtin_type(x_type, x_add, x, y)
if out is NotImplemented:
out = ignore_type_error_if_builtin_type(y_type, y_radd, y, x)
if out is NotImplemented:
raise TypeError('...')
return out
For custom types, this complexity causes addition to have about double the overhead of a direct method call:
import timeit
source = \
'''
class A:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + other.value
a = A(0)
b = A(1)
'''
timeit.timeit('a + b', source)
# 0.04903000299964333
timeit.timeit('add(a, b)', f'{source}\nadd=A.__add__')
# 0.029219600999567774

However, Python specializing adaptive interpreter speeds up the code for builtin types, such as int.
import timeit
timeit.timeit('a + b', 'a=0\nb=1')
# 0.009246169998732512
timeit.timeit('add(a, b)', 'a=0\nb=1\nadd=int.__add__')
# 0.04777865600044606

Monkey-Patching
Python supports monkey-patching; that is, the behavior of types and functions can be modified at runtime. For example, you can change the implementation of a type’s method:
class Dog:
def talk(self):
return 'Woof'
a = Dog()
print(a.talk()) # Prints 'Woof'
Dog.talk = lambda self: 'Meow'
print(a.talk()) # Prints 'Meow'
Monkey-patching prevents method inlining, an optimization technique where a method is copied into its call sites, removing call overhead and allowing further optimization to be done.
Lack of a JIT compiler
Python currently lacks a JIT compiler, meaning it cannot optimize code based on runtime characteristics (such as how often a branch is taken). This will probably change in the future, as a experimental JIT compiler for Python is currently being worked on.
However, all the above is insignificant to what was our biggest performance obstacle…
Proxies are slow
A proxy is a class that acts as a facade for arbitrary classes. In order for Java to call a Python method (or vice versa), a Foreign Function Interface must be used. A Foreign Function Interface, or FFI in short, is an interface that allows one program language to call functions of another. In Java, there are currently two different FFI implementations:
- Java Native Interface (JNI)
- (Java 22 and above) Foreign Function and Memory API (foreign)
JNI and the foreign API both returns a raw pointer (in JNI, a long, and in foreign, a MemorySegment) for Python objects. However, a pointer has little use in Java code, which expects interfaces like Comparable or Function. Typically, proxies are used to convert the pointer into Comparable, Function, or some other interface.
Why Proxies are slow
If a proxy is empty, it is only about 1.63 slower than a direct call. However, proxies are often not empty. Often, they need to do complex tasks such as converting the arguments and results of methods. How slow are we talking? Consider this simplistic benchmark:
public class Benchmark {
public static void benchmark(Runnable runner) {
var start = System.nanoTime();
for (int i = 0; i < 10_000; i++) {
runner.run();
}
var end = System.nanoTime();
System.out.printf("%.9f\n", (end - start) / 1_000_000_000d);
}
public static void main(String[] args) {
System.out.println("Cold");
benchmark(() -> {});
System.out.println("Hot");
benchmark(() -> {});
}
}
Which we can invoke via JPype
import jpype
jpype.startJVM(classpath=['.'])
benchmark = jpype.JClass('Benchmark')
@jpype.JImplements('java.lang.Runnable')
class Runner:
@jpype.JOverride
def run(self):
pass
print("Cold")
benchmark.benchmark(Runner())
print("Hot")
benchmark.benchmark(Runner())
or GraalPy:
import java.Benchmark
def run():
pass
print("Cold")
java.Benchmark.benchmark(run)
print("Hot")
java.Benchmark.benchmark(run)
Here are the runtimes for running benchmark with an empty Runnable in Java versus an empty Runnable implemented as a Python proxy:

Despite the Python code literally doing nothing, a JPype proxy is 967.102% slower than a direct Python call when hot. GraalPy fares better at only 100% slower than a direct Python call. Things get even worse when there are arguments and return types involved (for example, with java.util.function.Predicate). Here, we pass a Python list to Java (which calls a Python proxy function on each item in a list).
import jpype
jpype.startJVM(classpath=['.'])
benchmark = jpype.JClass('Benchmark')
class Item:
def __init__(self, value):
self.value = value
@jpype.JImplements('java.util.function.Predicate')
class Runner:
@jpype.JOverride
def test(self, item):
return item.value
def proxy(item):
return jpype.JProxy(jpype.JClass('java.io.Serializable'), inst=item, convert=True)
arraylist = jpype.JClass('java.util.ArrayList')
items = arraylist([proxy(Item(i % 2 == 0)) for i in range(10000)])
print('Cold')
benchmark.benchmark(Runner(), items)
print('Hot')
benchmark.benchmark(Runner(), items)
or GraalPy:
import java.Benchmark
class Item:
def __init__(self, value):
self.value = value
def test(item):
return item.value
items = [Item(i % 2 == 0) for i in range(10000)]
print("Cold")
java.Benchmark.benchmark(test, items)
print("Hot")
java.Benchmark.benchmark(test, items)

Here, JPype is 1402.41% slower than calling Python directly when hot! GraalPy also degraded, becoming 453.906% slower than calling Python directly.
In particular, when the arguments are passed to Java, they are converted to Java proxies. However, when the arguments are passed back to Python, they are not converted back to Python objects. Instead, a Python proxy is created for the Java proxy, creating additional overhead for every single operation.
Why using FFI directly is hard
Given the problems with proxies, the obvious solution is to just manage the opaque Python pointers and do direct Python calls. Unfortunately, using FFI directly is hard:
- In order for garbage collection to work, the Java and Python garbage collectors must be aware of each other (especially if Java objects are passed to Python). In particular, if an arena is closed, it is zeroed out (to prevent use-after-free). This means if Python is still tracking the object (because it retains a reference), a segmentation fault will occur on the next garbage collection. However, if an arena is never closed, we will eventually run out of memory. This is especially true for long running processes, such as solving. JPype solves this with deep magic that links Java’s and Python’s garbage collectors. Unfortunately, calling Python functions directly using FFI is unsupported in JPype.
- Java FFI is designed around starting from Java, not from Python (i.e. Java host). As a result, without doing deep magic like JPype, a separate Java process must be launched to use FFI with an independent Python interpreter.
- Java objects cannot be used as Python objects directly, meaning some proxies will need to be generated if a Java object is passed to Python code.
Avoiding all the problems (and replacing them with new ones)
So how can we avoid the performance losses from:
- Global Interpreter Lock
- Dynamically Typed Code
- Monkey Patching Support
- No JIT Compiler
- Proxy Overhead
with a single solution? The (not so simple) answer is to translate Python’s bytecode and objects to Java bytecode and objects.
- No Global Interpreter Lock, as Java doesn’t have one.
- Can treat type annotations as strict, meaning we can determine which method to call at compile time in some cases.
- Can remove monkey patching support; by the time the code is compiled, typically all monkey patching (if any) is already done.
- Java has a JIT compiler.
- Because the Python objects are translated to Java objects, no proxies are involved.
However, there is a not so simple part:
- Unlike Java, Python’s bytecode changes drastically from version to version. Even the behavior of existing opcodes may change (i.e. Python bytecode is neither forwards nor backwards compatible).
- Python’s bytecode has several quirks that makes it non-trivial to directly map it to Java bytecode. For instance, Python keeps its for loop’s iterator on the stack for the entire for loop body. As a result, when an exception is caught in a for loop, Python bytecode expects there to still be an iterator on the stack (whereas in Java, the exception handler’s stack is always empty).
- The Java object result needs to be converted back to a Python object.
Annoying as these problems are, they are significantly easier to debug and test than using FFI directly; we can rely on JPype’s deep magic to keep Java’s and Python’s garbage collectors in sync, and we can have deterministic reproducers and test cases.
How the translated bytecode performance?
Is it truly worth all the hassle of translating Python’s bytecode? Well, the only way to know is to measure the speed before and after the bytecode translation. Bytecode translation was originally implemented in OptaPy (which Timefold Solver for Python is a fork of), which included an option to use the proxies instead. Since the bytecode generation became more mature, the option to use proxies was removed from Timefold Solver for Python.
Thus, to compare the performance of proxies versus translated bytecode, we will use the latest (and probably last) release OptaPy (9.37.0b0) on Python 3.10.
Here are the score calculation speeds of using proxies versus translated bytecode in OptaPy’s school timetabling quickstart (higher is better).
[highlight]
Note: OptaPy’s School Timetabling quickstart is not equivalent to Timefold’s School Timetabling quickstart: there are bugs in the constraints that are fixed in the Timefold versions but were never addressed in OptaPy’s version.
[highlight]

In short, using translated bytecode multiplied our throughput by 100 (i.e. an over 100,000% performance increase)!
So… how fast is Timefold Solver for Python?
Now finally to the topic of the blog post: how fast is Timefold Solver for Python on various PlanningAI models? Below are the move evaluation speeds of the Java and Python Timefold 1.15.0 quickstarts:

In short:
- School Timetabling in Python is about 75% slower than Java
- Employee Scheduling in Python is about 70% slower than Java
- Vehicle Routing in Python is about 95% slower than Java
Where is performance lost?
If the Python bytecode is translated to Java bytecode, why is it still significantly slower than Java? There are several differences between the translated bytecode and normal Java bytecode:
- The translated bytecode must be consistent with Python’s semantics. This means:
- Cannot use 32-bit int or 64-bit long to represent a Python’s int, since Python’s int are
BigInteger. - Although some dynamic dispatches during operations such as addition can be determined statically, others cannot either due to missing or lost type information. In Java, all dispatches are determined at compile time.
- In the case a function expects a Java return type or argument, a conversion to the appropriate Java/Translated Python type must occur.
- Cannot use 32-bit int or 64-bit long to represent a Python’s int, since Python’s int are
- The translated bytecode is far from idiomatic Java bytecode. As such, the Java JIT compiler, designed for idiomatic Java bytecode, might have trouble optimizing it. Even Fernflower (a Java decompiler) has trouble decompiling the translated bytecode!
- Inlining opportunities are limited, due to the generated bytecode’s size and usage of interfaces.
What does the result mean in practice?
- For school timetabling and employee scheduling, to get the same result as Java in Python, you should run it about four times longer duration, (so if Java runs for 1 minutes, Python should run for 4 minutes).
- For vehicle routing, to get the same result as Java in Python, you should run it about twenty times longer duration, (so if Java runs for 1 minutes, Python should run for 20 minutes).
In many cases, this performance loss is acceptable, as it allows you to write your code in pure Python instead of Java, meaning:
- No requirement to learn Java.
- No requirement to make a separate Java model that matches your Python model.
- Can use Python libraries like Pydantic, FastAPI and Pandas to create the planning problem and to process solutions.
[highlight]
Note: Python libraries might use functions defined in C, which naturally have no corresponding Python bytecode, and thus cannot be translated to Java bytecode.
Usage of C‑defined functions in constraints causes a necessary fallback to Python, and the Java objects need to be converted back to Python objects. This causes a massively performance degradation that has roughly the same performance as proxies.
C‑defined functions can be used outside of constraints without issue.
[highlight]
Conclusion
To make Timefold’s goal of making PlanningAI accessible to everyone a reality, everyone should be able to write in the language of their choice. For many, that language is Python.
To make writing PlanningAI models in Python practical, the Python solver must match a reasonable fraction of the Java Solver’s performance. This is a challenge, as Python provides a myriad of obstacles to performance. But like any obstacle, they can be overcome.
By overcoming these obstacles, we managed to make the Python solver match 25% of the Java solver’s performance, making writing PlanningAI models in Python practical. That being said, for use cases that truly require the most performant code, it is probably best to stick to Java.

Timefold Secures €6 Million to expand Planning AI platform
Timefold, a Belgian AI software startup specializing in planning optimization, has successfully raised €6 million in a seed funding round led by Lakestar, one of Europe’s top venture capital firms. Returning investor Smartfin also participated in the round. This funding will accelerate the development and commercialization of Timefold’s groundbreaking PlanningAI technology, designed to optimize complex scheduling and routing challenges faced by large-scale enterprises.
The round follows a €2 million pre-seed funding secured in March 2023, led by Smartfin, which fueled the initial development and market introduction of Timefold's platform. The company’s software, based on the open-source OptaPlanner project, aims to solve inefficiencies in large-scale planning for industries such as logistics, transportation, field services, and workforce management.
Timefold’s platform is already trusted by several Fortune 500 companies, including Deutsche Bahn, Palantir, NEC, ADP, and Advantage Solutions. The new funding will enable Timefold to further scale its offerings, focusing on building out its platform and further developing planningAI models for other mission-critical applications.
A Game-Changing Solution for Scheduling Problems
Timefold’s PlanningAI platform is poised to eliminate the inefficiencies caused by inadequate planning solutions. Many businesses today face a choice between lengthy development cycles with custom-built models or underpowered off-the-shelf tools. Timefold offers a more powerful alternative—enterprise-ready models that can be easily integrated via REST APIs, reducing the need for expensive consultancy retainers or in-house expertise.
Geoffrey De Smet, CTO and co-founder of Timefold, highlighted the company’s vision:
"Planning is everywhere and often hidden in plain sight, yet efficient schedules are rare. From the field service technician coming to fix your boiler, to the plane assigned to your next flight: schedules run the world. We are tackling complex planning problems by providing out-of-the-box planning models for IT teams to run, test, and configure easily. It is easy to use, no math, and no need for consultants. Our API plugs seamlessly into your software, and with Lakestar’s backing, we can help businesses deploy planning that increases productivity, improves employee happiness, and reduces carbon emissions."
Proven Success and Growing Demand
Timefold is already making a significant impact across multiple industries. Companies like Palantir Technologies have partnered with Timefold to integrate their advanced scheduling optimizers across their Foundry platform.
"Palantir has collaborated with the Timefold team to make their world-class scheduling optimisers available across our Foundry and AI Platforms. The team’s experience, the depth of their solutions, and their willingness to dive into hard problems has enabled us to deliver significant outcomes for our customers. We are excited about their fundraise and look forward to scaling our impact together into the future,"
Molly Carmody, Planning Optimisation Lead at Palantir
Backed by Industry Leaders
"Geoffrey's life's work has been to revolutionize how complex NP-hard problems are addressed, saving enterprises, governments, and infrastructure providers hundreds of millions of euros annually. For 18 years, his leading open-source technology has optimized critical operations, scheduling shifts for thousands of police officers, routing millions of parcels, assigning gates at major airports, and planning national train timetables."
When we met Geoffrey, Maarten, and the rest of the Timefold team, we recognized how uniquely talented this team is and the outsized potential of a category winner in the making.
Enrico Mellis, principal at Lakestar
Mellis added:
"Maarten brings vital entrepreneurial expertise to propel Geoffrey’s vision to market. The solution's unmatched maturity and clear product-market fit, bolstered by partnerships with giants like Palantir, promise to transform operations across multiple industries."
Leadership and Growth
Timefold’s executive team includes industry veterans who have joined from top software companies like Showpad, Tinder, Qualtrics, and Techwolf, further strengthening its position to drive innovation and growth. CEO and co-founder Maarten Vandenbroucke commented:
"Typically, companies are left with two extreme choices: build their own models which take long development cycles and costly consultancy retainers, or use underpowered automation tools. With Timefold, neither is necessary. We have enterprise-ready models for field service routing, employee scheduling, production planning, and more in development. Our clients are already achieving better outcomes and faster time-to-market, which significantly reduces risk from their end."

Simplify the Shadow Variable Listener Implementation
Imagine reducing your code by 90% while making your optimization models more efficient and easier to maintain. Sounds impossible? Not anymore. With Timefold’s new @CascadingUpdateShadowVariable, you can streamline complex planning tasks—like updating the arrival times in a vehicle route—into just a few lines of code. In this article, I’ll show you how this powerful new feature can simplify your codebase, enhance performance, and free you from the tedious task of writing custom listeners. We will demonstrate how to simplify the Vehicle Route Problem (VRP), turning 60 lines of code into 5.

Timefold Solver has a useful feature called shadow variables. A shadow variable is a planning variable that can be deduced from the state of genuine planning variables, such as the arrival time of a given visit or the following visit of a vehicle route. The shadow variable @ShadowVariable provides an approach that allows implementing a custom listener to update the variable values.
Before Timefold Solver 1.13.0, you would update a chain of connected elements using the @ShadowVariable listener. This robust solution can be applied to any related use case. However, at Timefold, we aim to simplify your life by offering solutions that enhance your models' understandability and improve their performance. And now we’ve made it so that creating a separate listener to update source variables is sometimes unnecessary.
The upcoming sections will initially discuss the requirements for updating the tail chains. Following that, the different approaches for configuring a listener that updates a specified source shadow variable and triggers changes to the subsequent elements of a planning list variable will be explained.
Tail chains need updating
Let’s consider the VRP problem definition. Given a set of vehicles and a list of locations to visit, each visit carries the expected arrival time to the destination. We estimate the arrival time based on the arrival time to the previous location and the travel time between locations.

In the previous image, the vehicle arrives at Location A at 08:00. After that, it travels to Location B, arriving at 09:00, and then travels to Location C. We have a series of related locations that need to be updated sequentially. That means if there are any changes in the arrival time at Location A, all subsequent locations must update their arrival times. In other words, a tail chain update is needed.
To illustrate the use case, let’s take a look at the Visit model class:
public class Visit {
...
@InverseRelationShadowVariable(sourceVariableName = "visits")
private Vehicle vehicle;
@PreviousElementShadowVariable(sourceVariableName = "visits")
private Visit previousVisit;
@NextElementShadowVariable(sourceVariableName = "visits")
private Visit nextVisit;
...
private LocalDateTime arrivalTime;
}
The arrivalTime field contains the vehicle’s arrival time, which the constraint can utilize to impose penalties for missing deadlines. The next two sections explain how to use the @ShadowVariable and @CascadingUpdateShadowVariable listeners, respectively.
The old way: implementing shadow variable listeners
The @ShadowVariable triggers a listener when one or more source shadow variables change. Let’s update the model Visit:
public class Visit {
...
@InverseRelationShadowVariable(sourceVariableName = "visits")
private Vehicle vehicle;
@PreviousElementShadowVariable(sourceVariableName = "visits")
private Visit previousVisit;
@NextElementShadowVariable(sourceVariableName = "visits")
private Visit nextVisit;
@ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "vehicle")
@ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "previousVisit")
private LocalDateTime arrivalTime;
}
Next, let’s implement the ArrivalTimeUpdatingVariableListener:
public class ArrivalTimeUpdatingVariableListener
implements VariableListener<VehicleRoutePlan, Visit> {
...
@Override
public void afterVariableChanged(ScoreDirector<VehicleRoutePlan> scoreDirector, Visit visit) {
if (visit.getVehicle() == null) {
if (visit.getArrivalTime() != null) {
scoreDirector.beforeVariableChanged(visit, ARRIVAL_TIME_FIELD);
visit.setArrivalTime(null);
scoreDirector.afterVariableChanged(visit, ARRIVAL_TIME_FIELD);
}
return;
}
Visit previousVisit = visit.getPreviousVisit();
LocalDateTime departureTime =
previousVisit == null ? visit.getVehicle().getDepartureTime() : previousVisit.getDepartureTime();
Visit nextVisit = visit;
LocalDateTime arrivalTime = calculateArrivalTime(nextVisit, departureTime);
while (nextVisit != null && !Objects.equals(nextVisit.getArrivalTime(), arrivalTime)) {
scoreDirector.beforeVariableChanged(nextVisit, ARRIVAL_TIME_FIELD);
nextVisit.setArrivalTime(arrivalTime);
scoreDirector.afterVariableChanged(nextVisit, ARRIVAL_TIME_FIELD);
departureTime = nextVisit.getDepartureTime();
nextVisit = nextVisit.getNextVisit();
arrivalTime = calculateArrivalTime(nextVisit, departureTime);
}
}
...
}
Whenever the vehicle or previousVisit changes, the listener automatically updates the subsequent visits.
The new way: simplifying with Cascading Updates
As of Timefold Solver 1.13.0, the @CascadingUpdateShadowVariable does not require a separate listener class. Instead, we add a new method to the domain class that updates the related shadow variables. Let’s update the Visit class with the cascading shadow variable annotation:
public class Visit {
...
@InverseRelationShadowVariable(sourceVariableName = "visits")
private Vehicle vehicle;
@PreviousElementShadowVariable(sourceVariableName = "visits")
private Visit previousVisit;
@NextElementShadowVariable(sourceVariableName = "visits")
private Visit nextVisit;
@CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime")
private LocalDateTime arrivalTime;
}
After that, we need to add a method called updateArrivalTime with the logic to update arrivalTime:
private void updateArrivalTime() {
if (previousVisit == null && vehicle == null) {
arrivalTime = null;
return;
}
LocalDateTime departureTime = previousVisit == null ? vehicle.getDepartureTime() : previousVisit.getDepartureTime();
arrivalTime = departureTime != null ? departureTime.plusSeconds(getDrivingTimeSecondsFromPreviousStandstill()) : null;
}
The method must not be static and must not accept any parameters. Timefold Solver triggers updateArrivalTime after all events are processed. Therefore, the listener will be the last one executed during the event lifecycle. Additionally, it automatically propagates changes to the subsequent visits and stops when the arrivalTime value does not change or when it reaches the end.

Conclusion
Updating a set of interconnected elements in a specific order is a common scenario when defining your optimization model. Timefold Solver provides different solutions for defining listeners that update shadow variable sources in a specific sequence. The new Cascading Update Shadow Variable simplifies creating listeners for planning list variables, resulting in code that is easier to write, read, and maintain.

Load balancing and fairness in constraints
Load balancing is a common constraint for many Timefold use cases. Especially when scheduling employees, the workload needs to be spread out fairly. For example, nurses in your hospital care that everyone works the same number of weekends, and there may even be legal requirements to that effect. Join us as we show you how to bring fairness to your Timefold solution.
Defining fairness
Fairness is a complex concept, and it is not always clear what is fair. For example, let’s assign 15 undesirable tasks to five employees. Each task takes a day, but the tasks have different skill requirements. In a perfectly fair schedule, each employee will get three tasks, because the average is 15 / 5 = 3. Unfortunately, this doesn’t solve the entire problem, because there are other constraints. For example, there are seven tasks that require a skill which only two of the employees possess. One of them will have to do at least four tasks.
From the above, we can see that our schedule cannot be perfectly fair, but we can make it as fair as possible. We can define fairness in two opposing ways:
- A schedule is fair if most users think it is fair to them.
- A schedule is fair if the employee with the most tasks has as few tasks as possible.
Since we want to treat all employees equally, the second definition is correct. Besides, if we make almost everyone happy by letting one employee do all the work, that employee would probably quit, and that doesn’t help.
Let’s look at a few different solutions of the same dataset, sorted from most to least fair. Each one has 15 tasks:
AnnBethCarlDanEdSolution QualitySchedule A33333Most fairSchedule B44322 Schedule C53322 Schedule D55221 Schedule E63321 Schedule F56211 Schedule G150000Most unfair
Ann has the most tasks each time. How do we compare schedules in which Ann has the same number of tasks?
AnnBethCarlDanEdSolution QualitySchedule C53322Schedule D55221
We look at the second most loaded employee, Beth. In schedule D, she has five tasks, while in schedule C, she has less. This makes schedule C fairer overall. Therefore, this is the definition of fairness we use in the remainder of this post.
Measuring unfairness
For the mathematically minded among you, we’re currently using the following formula to calculate the unfairness of a solution, also called "squared deviation from the mean":
In this formula:
nis the number of employees.xiis the number of tasks assigned to employeei.x̄is the average number of tasks per employee.
Also see a MathExchange question discussing this formula and other alternatives in depth.
Implementing fairness constraints
Timefold Solver supports fairness constraints out of the box through the use of grouping and the load-balancing constraint collector. In your ConstraintProviderimplementation, a load-balancing constraint may look like this:
Java
Constraint balanceEmployeeShiftAssignments(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Shift.class)
.groupBy(ConstraintCollectors.loadBalance(Shift::getEmployee))
.penalizeBigDecimal(HardSoftBigDecimalScore.ONE_SOFT, LoadBalance::unfairness)
.asConstraint("Balance employee shift assignments");
}
Kotlin
fun balanceEmployeeShiftAssignments(constraintFactory: ConstraintFactory): Constraint {
return constraintFactory.forEach(Shift::class.java)
.groupBy(ConstraintCollectors.loadBalance({ shift -> shift.employee }))
.penalizeBigDecimal(HardSoftBigDecimalScore.ONE_SOFT) { loadBalance -> loadBalance.unfairness }
.asConstraint("Balance employee shift assignments")
}
Python
def balance_employee_shift_assignments(constraint_factory: ConstraintFactory):
return (constraint_factory.for_each(Shift)
.group_by(ConstraintCollectors.load_balance(lambda shift: shift.employee))
.penalize_decimal(HardSoftDecimalScore.ONE_SOFT, lambda load_balance: load_balance.unfairness())
.as_constraint("Balance employee shift assignments")
)
This constraint will penalize the solution based on its unfairness, defined in this case by how many ShiftAssignment are allocated to any particular Employee.
Unfairness is a dimensionless number that measures how fair the solution is. A lower value indicates a fairer solution. The smallest possible value is 0, indicating that the solution is perfectly fair. There is no upper limit to the value, and it is expected to scale linearly with the size of your dataset. The value is rounded to six decimal places.
Different unfairness values may only be compared for solutions coming from the same dataset. Unfairness values computed from different datasets are incomparable.
Important: When using fairness constraints, we recommend that you use one of the score types based on BigDecimal, such as HardSoftBigDecimalScore. See Timefold documentation for a detailed rationale.
Conclusion
Fairness in your planning improves employee satisfaction and helps you comply with legal requirements. Adding fairness to your Timefold solution is straightforward and can be done in a few lines of Java, Python or Kotlin code. Other solvers would be hard-pressed to match the flexibility of our implementation. Check out the latest Timefold Solver and start making your solutions fairer today.

When scheduling works, everything works.
Less waste. More control. Teams that trust the plan.