Designing the Timefold logo
Every new company needs a logo. So does Timefold. When starting a company, one of the most fun tasks is creating the logo. Far more exciting than VAT accounting, I can tell you.
Of course, a good logo is important: It’s hard to change. It must be able to stand the test of time. It should fit on a variety of media. So we hired a professional to help us: Ivo Blomme, a digital and brand designer with years of experience. He took us through the process of creating a good logo. Based on our input, he created these:

The left one is based on the rectangles of a physical calendar. The right one, with a fold in it, is based on a fold in space and time. Which logo would you have picked?
It was a though call, but we ended going for the second one. It’s loosely based on the idea of folding space and time:

Our designer even showed us how it would look on digital devices:

Now, we don’t have any plans to create a mobile app. But it would look good, wouldn’t it?

How upskilling technicians unlocks field service routing efficiency
In telecom, utilities, and HVAC, not every technician can handle every job. A field engineer certified for fiber installations may or may not also hold IP networking qualifications. These skill gaps seem like a workforce planning issue, but they compound into a routing problem that costs companies millions in unnecessary travel time and lost capacity.
The question worth asking: how much money are you leaving on the table? We've modeled it across multiple scenarios and quantified the answer in dollars.
The problem
Consider a simplified but representative example: 5 service visits, 2 technicians, each visit requiring one specific skill, each technician holding one skill.

The routing algorithm has no choice but to send both technicians across the city to match skills to jobs, even when a geographically closer technician could handle the work with a single additional qualification.
The result is excessive travel time, reduced daily capacity, and lower customer face time.
The opportunity
Now consider what changes when one technician is cross-trained.

With the red technician qualified for network jobs as well, the routing engine can assign jobs more efficiently, and the other technician's route contracts immediately.
Extend that to both technicians holding all relevant skills, and both routes shorten substantially.

Total travel time drops significantly, freeing capacity for additional visits or higher-quality customer interactions. The productivity gain is real and measurable, not theoretical.
The benchmarks
To move beyond simplified examples, we modeled this against a Los Angeles dataset with 1,012 service visits, 253 technicians, and three distinct skill types. Each job requires exactly one skill, but coverage of technician skills varies.
We ran three scenarios to simulate progressive upskilling investment:
- 1 skill per technician (baseline): No cross-training. Each technician holds exactly one qualification.
- 2 skills per technician: Every technician is trained on one additional skill.
- 3 skills per technician: Full cross-training. Every technician holds all three qualifications.
All three scenarios were solved using our Field Service Routing API.

The results
The productivity gains from upskilling are significant and consistent.

Moving from 1 to 2 skills per technician reduces travel time by 23%. For a technician averaging 2 hours of daily drive time, that's over 26 minutes saved per day, which compounds to 88 hours per year at 200 working days. At a $50/hour wage rate, that amounts to $4,400 in recovered productivity per technician annually.

Moving from 2 to 3 skills reduces travel time by a further 17%, adding another $3,400 per technician per year.
Partial upskilling
Partial upskilling still delivers meaningful ROI. Training only half the workforce on a second skill still yields a 14% reduction in travel time: $2,800 per technician per year across the organization.

Interestingly, the ROI in this scenario reaches $5,600 per trained technician annually, as the routing engine concentrates efficiency gains through the newly cross-trained subset. The aggregate company-wide return is lower, but the per-investment return is higher; a useful lever for phased rollout decisions.
Conclusion
This analysis is based on a single dataset from a single metropolitan region. Field operations at an enterprise scale are considerably more complex, involving non-uniform skill distributions, varying geographic densities, and routing challenges across hundreds of service regions and time periods.
This study demonstrates the structure of the ROI opportunity. The specific numbers will vary with your workforce composition and operational footprint, but the finding is consistent with what we observe across our customer base.
The most accurate projections come from running these simulations against your own data. We can do exactly that and give you a defensible, operations-specific estimate of the value of upskilling investments before you make them.

How much fuel can route optimization actually save?
Fuel costs are rarely the largest expense on a field service balance sheet, but they are often the most visible indicator of operational waste. For most fleet managers, the primary challenge is not deciding whether to optimize. The real challenge is determining exactly how much margin is being lost to the road and how to capture that value in a way that satisfies a CFO.
That second part matters more than vendors usually admit. After deploying optimization, fleets often see metrics improve in some places and degrade in others. Before deciding on scheduling optimization, it is worth knowing what the typical savings range looks like, what drives the spread, and how to measure savings in a way that survives a CFO review.
What savings to expect
Across published case studies and operator interviews, field service operations moving from dispatcher-built routes to automated route optimization typically see drive time fall 15% to 30%. Fuel consumption tracks closely, with reported savings of 10% to 25% depending on territory geometry and stop density.
Two things drive the spread:
- Starting point. Fleets coming off paper routes or single-dispatcher Excel tend to land at the high end. Operations already running a basic FSM with simple capacity rules often start at 5% to 10% savings and grow from there as constraints get tuned.
- Constraint stack. A fleet with skill matching, time windows, SLAs, and overtime rules is already hard manage manually. Add multi-vehicle stops, dependent jobs, or rolling time-windows, and it becomes impossible. So, the complexer the operations, the harder it is for the human planner to zoom out, or to think about efficiency. In these cases, a feasible schedule is already a big win, but fuel costs are not something taken into account.
According to Service Council research, the cost to dispatch a technician ranges from $250 in urban environments to as high as $2,500 for rural or multi-day jobs. When you combine this with the US National average fuel price hovering at $4.50 per gallon (May 2nd, 2026),high costs are draining your service profits.
How to measure routing optimization savings credibly
Three traps operations leaders fall into:
- Comparing optimized days against manual days that were not comparable. Manual routing tends to do better on light days and worse on heavy days. If the rollout coincides with a seasonal lull, the comparison flatters the optimizer. The fix: index by stops-per-day or jobs-per-tech, not raw miles.
- Counting gross miles, not the right miles. "Miles driven" includes deadhead, on-job movement, and home-to-first-stop legs. Optimization should mostly cut the first two. If you measure all three together, returns look smaller than they are. Pull a week of telematics data and split it before you set the baseline.
- Relying on "Holdout" territories. Comparing one territory against another is often flawed because no two regions have the same density or traffic patterns. The cleanest measurement approach is a parallel plan analysis: run your manual process as usual, but simultaneously feed the exact same job data into the optimizer. Compare the two resulting schedules for the same day to see the true delta in miles and cost.
Where different tools fit in the fuel-cost picture
The market often gets discussed as if every "route optimization" product is the same thing. They are not. Four product categories show up in fuel-reduction conversations, each doing a different job:
Map / matrix APIGoogle Maps PlatformReturns travel times and distances; routes a given sequence.Indirect: provides the data layer optimizers depend on.CategoryExampleWhat it actually doesWhere it moves fuelField service management (FSM)Salesforce Field Service, ServiceNow, ServiceMax,...End-to-end work order, dispatch, and mobile ops.Direct, but limited to constraints and tuning the FSM exposes.Scheduling APITimefold Field Service RoutingOptimization-as-an-API: feeds in jobs, skills, certificates, time windows, SLAs,...Direct, designed for service constraints; embeds into existing stacks.
The right pick depends on what you already have. If you are running Salesforce Field Service and your dispatch lives there, the question is whether the bundled optimizer is hitting the savings range above. If not, you might need to call out to a scheduling API to fill the gap. If you are running a homegrown ops stack, you need a solution that provides both the distance data and the solver engine to find the best routes.
Three questions to expose if a vendor can deliver
These questions help separate vendors that can actually deliver savings from those that just quote them:
- Can your engine support all our hard constraints simultaneously? If an optimizer cannot handle every real-world rule (skills, windows, SLAs) at the same time, it will produce "invalid" routes. When a planner has to manually fix these, any theoretical fuel savings immediately vanish.
- How can the schedule adapt to real-world events? Most fuel waste happens after disruptions like sick technicians, overrun jobs, or cancellations. If the engine cannot re-solve the schedule in seconds during the day, the efficiency you gained in the morning is lost by lunch.
- How do we prove the savings during a pilot? Avoid "before and after" comparisons. Ask the vendor to perform a "shadow" run where they optimize a historical week of your real data and compare it against the actual routes your team drove.
Fuel cost is the easiest win in field service. The savings are real, the math is settled, and the technology is mature. What is left is fit: with your stack, your constraints, and how your team actually runs.

Three optimizations to make your Timefold Solver faster
Our friends at Dots & Lines recently claimed they made their Timefold Solver 17x faster and they're not wrong. With our IncrementalScoreCalculator, you can absolutely get there. But it comes bundled with months of effort, sacrificed explainability, and constraints that become a nightmare to maintain or extend.
There's a better way. In this post, we'll apply three targeted optimizations to the same course scheduling problem they used and walk away with a 91% speedup while keeping your constraints clean, testable, and easy to change.
The Setup
We're using the code from the Dots & Lines article as our baseline. If you haven't read it, don't worry, we'll cover everything you need here. One difference: we're measuring things slightly differently. We're using the comp06 dataset (where they saw the biggest gains), running for 1 minute with a 15-second JVM warmup, averaging across 10 runs in separate JVM processes, and using -Xmx1g -XX:+UseParallelGC -XX:+UseCompactObjectHeaders to fine-tune performance and keep the noise out. This gives us a more stable baseline than a single long run.
Here's the starting constraint implementation:
public class CurriculumCourseConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[]{
conflictingLecturesDifferentCourseInSamePeriod(factory),
conflictingLecturesSameCourseInSamePeriod(factory),
roomOccupancy(factory),
unavailablePeriodPenalty(factory),
roomCapacity(factory),
minimumWorkingDays(factory),
curriculumCompactness(factory),
roomStability(factory)
};
}
// ************************************************************************
// Hard constraints
// ************************************************************************
Constraint conflictingLecturesDifferentCourseInSamePeriod(ConstraintFactory factory) {
return factory.forEach(CourseConflict.class)
.join(Lecture.class,
equal(CourseConflict::getLeftCourse, Lecture::getCourse))
.join(Lecture.class,
equal((courseConflict, lecture1) -> courseConflict.getRightCourse(), Lecture::getCourse),
equal((courseConflict, lecture1) -> lecture1.getPeriod(), Lecture::getPeriod))
.filter(((courseConflict, lecture1, lecture2) -> lecture1 != lecture2))
.penalize(ONE_HARD,
(courseConflict, lecture1, lecture2) -> courseConflict.getConflictCount())
.asConstraint("conflictingLecturesDifferentCourseInSamePeriod");
}
Constraint conflictingLecturesSameCourseInSamePeriod(ConstraintFactory factory) {
return factory.forEachUniquePair(Lecture.class,
equal(Lecture::getPeriod),
equal(Lecture::getCourse))
.penalize(ONE_HARD,
(lecture1, lecture2) -> 1 + lecture1.getCurriculumSet().size())
.asConstraint("conflictingLecturesSameCourseInSamePeriod");
}
Constraint roomOccupancy(ConstraintFactory factory) {
return factory.forEachUniquePair(Lecture.class,
equal(Lecture::getRoom),
equal(Lecture::getPeriod))
.penalize(ONE_HARD)
.asConstraint("roomOccupancy");
}
Constraint unavailablePeriodPenalty(ConstraintFactory factory) {
return factory.forEach(UnavailablePeriodPenalty.class)
.join(Lecture.class,
equal(UnavailablePeriodPenalty::getCourse, Lecture::getCourse),
equal(UnavailablePeriodPenalty::getPeriod, Lecture::getPeriod))
.penalize(ofHard(10))
.asConstraint("unavailablePeriodPenalty");
}
// ************************************************************************
// Soft constraints
// ************************************************************************
Constraint roomCapacity(ConstraintFactory factory) {
return factory.forEach(Lecture.class)
.filter(lecture -> lecture.getStudentSize() > lecture.getRoom().getCapacity())
.penalize(ofSoft(1),
lecture -> lecture.getStudentSize() - lecture.getRoom().getCapacity())
.asConstraint("roomCapacity");
}
Constraint minimumWorkingDays(ConstraintFactory factory) {
return factory.forEach(Lecture.class)
.groupBy(Lecture::getCourse, countDistinct(Lecture::getDay))
.filter((course, dayCount) -> course.getMinWorkingDaySize() > dayCount)
.penalize(ofSoft(5),
(course, dayCount) -> course.getMinWorkingDaySize() - dayCount)
.asConstraint("minimumWorkingDays");
}
Constraint curriculumCompactness(ConstraintFactory factory) {
return factory.forEach(Curriculum.class)
.join(Lecture.class,
filtering((curriculum, lecture) -> lecture.getCurriculumSet().contains(curriculum)))
.ifNotExists(Lecture.class,
equal((curriculum, lecture) -> lecture.getDay(), Lecture::getDay),
equal((curriculum, lecture) -> lecture.getTimeslotIndex(), lecture -> lecture.getTimeslotIndex() + 1),
filtering((curriculum, lectureA, lectureB) -> lectureB.getCurriculumSet().contains(curriculum)))
.ifNotExists(Lecture.class,
equal((curriculum, lecture) -> lecture.getDay(), Lecture::getDay),
equal((curriculum, lecture) -> lecture.getTimeslotIndex(), lecture -> lecture.getTimeslotIndex() - 1),
filtering((curriculum, lectureA, lectureB) -> lectureB.getCurriculumSet().contains(curriculum)))
.penalize(ofSoft(2))
.asConstraint("curriculumCompactness");
}
Constraint roomStability(ConstraintFactory factory) {
return factory.forEach(Lecture.class)
.groupBy(Lecture::getCourse, countDistinct(Lecture::getRoom))
.filter((course, roomCount) -> roomCount > 1)
.penalize(HardSoftScore.ONE_SOFT,
(course, roomCount) -> roomCount - 1)
.asConstraint("roomStability");
}
}
With that out of the way, let's get to the optimizations!
Upgrading to the latest Timefold Solver
The original article used Timefold Solver 1.21.0, a version which is more than a year old at the time of writing this article. Since then, there have been many optimizations and bugfixes to the Timefold Solver codebase, and upgrading to the latest version of Timefold Solver is the first step to get optimal performance.
While upgrading to the newest Timefold Solver version did not significantly impact baseline performance in this use case, the newer versions do come with extra features you can implement to improve performance.
Precomputing static joins
Let's consider the conflictingLecturesDifferentCourseInSamePeriod constraint:
return factory.forEach(CourseConflict.class)
.join(Lecture.class,
equal(CourseConflict::getLeftCourse, Lecture::getCourse))
.join(Lecture.class,
equal((courseConflict, lecture1) -> courseConflict.getRightCourse(), Lecture::getCourse),
equal((courseConflict, lecture1) -> lecture1.getPeriod(), Lecture::getPeriod))
.filter(((courseConflict, lecture1, lecture2) -> lecture1 != lecture2))
.penalize(ONE_HARD,
(courseConflict, lecture1, lecture2) -> courseConflict.getConflictCount())
.asConstraint("conflictingLecturesDifferentCourseInSamePeriod");
This constraint has two joins - the first join is between CourseConflict and Lecture, and the second join is between the result of the first join and Lecture again. The first join is static. It only depends on the problem facts and does not depend on any variables. This means that the product of this join will never change during solving, and we can therefore cache it before solving, using a recently introduced precomputation feature of Timefold Solver. The adapted constraint would look like so:
return factory.precompute(
f -> f.forEachUnfiltered(Curriculum.class)
.join(Lecture.class,
filtering((curriculum, lecture) -> lecture.getCurriculumSet().contains(curriculum))))
.join(Lecture.class,
equal((courseConflict, lecture1) -> courseConflict.getRightCourse(), Lecture::getCourse),
equal((courseConflict, lecture1) -> lecture1.getPeriod(), Lecture::getPeriod))
.filter(((courseConflict, lecture1, lecture2) -> lecture1 != lecture2))
.penalize(ONE_HARD,
(courseConflict, lecture1, lecture2) -> courseConflict.getConflictCount())
.asConstraint("conflictingLecturesDifferentCourseInSamePeriod");
And what do we get in return for this small change? A nice 17% speedup!
VersionMoves Per secondSpeedBaseline799941.00x (baseline)Precomputed join939371.17x
As is often the case, the best join is a join avoided. Can we avoid more?
Consecutive sequences
The curriculumCompactness constraint makes sure that lectures of the same curriculum are scheduled in consecutive timeslots. In order to do this, it selects all lectures of a curriculum and checks for each lecture if there is another lecture of the same curriculum in the previous or next timeslot. If there is not, a penalty will ensue.
return factory.forEach(Curriculum.class)
.join(Lecture.class,
filtering((curriculum, lecture) -> lecture.getCurriculumSet().contains(curriculum)))
.ifNotExists(Lecture.class,
equal((curriculum, lecture) -> lecture.getDay(), Lecture::getDay),
equal((curriculum, lecture) -> lecture.getTimeslotIndex(), lecture -> lecture.getTimeslotIndex() + 1),
filtering((curriculum, lectureA, lectureB) -> lectureB.getCurriculumSet().contains(curriculum)))
.ifNotExists(Lecture.class,
equal((curriculum, lecture) -> lecture.getDay(), Lecture::getDay),
equal((curriculum, lecture) -> lecture.getTimeslotIndex(), lecture -> lecture.getTimeslotIndex() - 1),
filtering((curriculum, lectureA, lectureB) -> lectureB.getCurriculumSet().contains(curriculum)))
.penalize(ofSoft(2))
.asConstraint("curriculumCompactness");
This is effectively three joins between Curriculum and Lecture with a filter on the timeslot index. If we can rewrite this constraint to avoid these joins, we can get a significant speedup. The adapted constraint could look like this:
return factory.precompute(CurriculumCourseConstraintProvider::curriculumLectureLeft)
.groupBy((curriculum, lecture) -> curriculum,
(curriculum, lecture) -> lecture.getDay(),
ConstraintCollectors.conditionally(
(curriculum, lecture) -> lecture.getDay() != null,
ConstraintCollectors.toConsecutiveSequences(
(Curriculum curriculum, Lecture lecture) -> lecture,
Lecture::getTimeslotIndex)))
.flattenLast(SequenceChain::getConsecutiveSequences)
.filter((curriculum, day, sequence) -> sequence.getLength() == 1)
.penalize(ofSoft(2))
.asConstraint("curriculumCompactness");
We first take advantage of the fact that much of this constraint is shared with the previous constraint. Therefore, we can reuse the same precomputation to get the lectures of each curriculum. Then we can split these lectures by the curriculum and the day that they fall on and then use the advanced toConsecutiveSequences collector to get the consecutive sequences of lectures for each curriculum and day. If a sequence has a length of 1, it means that there is a lecture that is not scheduled next to any other lecture of the same curriculum, and therefore we need to apply a penalty.
The constraint is now a bit more difficult to read, but we get a 40% speedup for our efforts!
VersionMoves Per secondSpeedBaseline799941.00x (baseline)Precomputed join939371.17xConsecutive Sequences1352011.69x
In performance optimizations, this is a common pattern: an obvious and straightforward implementation is often not the most performant one. But can we still do better than this?
Replacing unique pairs with maths
The roomOccupancy constraint checks if there are two lectures which share the same room and the same period. This is clearly an impossible situation, and therefore needs to be penalized. The constraint gets it done by penalizing every unique pair of lectures that share the same room and the same period:
return factory.forEachUniquePair(Lecture.class,
equal(Lecture::getRoom),
equal(Lecture::getPeriod))
.penalize(ONE_HARD)
.asConstraint("roomOccupancy");
This is a join, and joins can be expensive when the cross-product becomes large. But those of you who paid attention in maths class might have noticed that this is actually a combinatorial problem. If there are n lectures in the same room and period, there are n choose 2 unique pairs between those lectures, which is equal to n * (n - 1) / 2. This means that we can rewrite the constraint to list all lectures, group them by room and period, count the number of lectures in each group, and apply the combinatorial formula to calculate the number of unique pairs in each group. The adapted constraint would look like this:
return factory.forEach(Lecture.class)
.groupBy(Lecture::getRoom, Lecture::getPeriod, count())
.filter((room, period, count) -> count > 1)
.penalize(ONE_HARD, (room, period, count) -> (count * (count - 1)) / 2)
.asConstraint("roomOccupancy");
Another join replaced, this time for an additional 13% speedup!
VersionMoves Per secondSpeedBaseline799941.00x (baseline)Precomputed join939371.17xConsecutive Sequences1352011.69xforEachUnique -> maths1524911.91x
What we achieved
Three optimizations. A few hours of work. A 91% speedup and the constraints are still readable, testable, and easy to extend. Compare that to the IncrementalScoreCalculator route: weeks of implementation, no out-of-the-box explainability without serious effort, and constraints that are genuinely painful to add or change. That's not a small thing.
The 17x headline is real, but it is the ceiling of what's possible after an enormous investment. We got to 2x in an afternoon, and that may be just enough.
When to optimize (and when to stop)
Performance work is always about tradeoffs. Here's the approach we'd recommend:
- Start clean: Write straightforward Constraint Streams code first. It's readable, testable, and fast enough for most problems.
- Don't optimize prematurely: If your solver is fast enough, ship it. Every optimization adds complexity.
- Profile before guessing: When you do need to optimize, use Constraint Profiling to find the actual bottlenecks...they're rarely where you expect.
- Optimize one thing at a time: Apply the highest-impact change, benchmark it, then decide if you need more.
- Know when to stop: Diminishing returns kick in fast. A 91% speedup from three targeted changes may already beat chasing a full rewrite. We'd only recommend reaching for
IncrementalScoreCalculatorif you've genuinely exhausted everything else and still can't hit your performance targets. For most problems, you ain't gonna need it. What you will need is ease of maintenance, and the ability to add new constraints without breaking a sweat.

Timefold Solver 2.0 upgrade: 15 projects, under 10 minutes
Timefold Solver 2.0 is now live. We realize that a new major version often looks scary and the natural question is how much work is this upgrade actually going to be?
Before asking anyone else to upgrade, we ran the migration playbook on all of our quickstart projects and kept a stopwatch running. Under 10 minutes, all done! No AI was used here, but a coding agent could perhaps make that even faster.
The quickstarts are small, but they cover most of the API surface, and we use them as a test bench for experiments. That makes them a pretty good benchmark for what the upgrade actually feels like.
The automated migration recipe
When we started on Solver 2.0, one of our goals was to leave some older deprecated APIs behind and clean up the package structure while we were at it. The other goal was to not make any of that your problem.
So we made sure almost all of the changes could be automated. OpenRewrite allowed us to create a recipe to deterministically perform the necessary changes to migrate to Solver 2.0. The same recipe can be used by all our users to run the same migration on their codebases with a single command:
# This blog post is static, check the documentation for the latest versions.
mvn org.openrewrite.maven:rewrite-maven-plugin:6.28.1:run \\
-Drewrite.recipeArtifactCoordinates=ai.timefold.solver:timefold-solver-migration:2.0.0 \\
-Drewrite.activeRecipes=ai.timefold.solver.migration.ToLatest
That one command pulls the latest recipe and upgrades Timefold Solver to 2.0.0. Running it across all quickstart projects took about 3 minutes.
Where the other 7 minutes went
With still 7 minutes on the clock, this is where the rest of that time went.
Java 21: < 1 minute
Solver 2.0 moves the Java baseline from 17 to 21. Simple find-and-replace in our build files.
The recipe doesn't change your Java version for you, because plenty of teams are already on something newer. We'd rather not touch that.
ConstraintCollectors change: ~ 2 minutes
Two of our quickstart projects (Employee Scheduling and Order Picking) used grouping functions in their constraints, which have changed in Solver 2.0. count() and countDistinct() used to return Integer. They now return Long. sum(ToIntFunction) is gone and the recipe migrates this to sum(ToLongFunction).
The recipe rewrites the collector calls themselves, but it can't fix the downstream code that assumed an int return type. The compiler errors are clear enough that we fixed these by hand in a couple of minutes. Pointing a coding agent at them would work just as well.
Running the tests: ~ 4 minutes
To confirm nothing broke, we ran the tests. With 15 projects and integration tests in the mix, that takes about 4 minutes.
Stop the clock! Green build! Migration Status == COMPLETE.
What this means for your project
While our quickstarts are small and your project is probably a lot bigger, the automated migration scripts can take you a long way. Only 2 of our 15 quickstart projects needed manual intervention, and even then it was minor.
If you are currently on Timefold Solver 1.x, we encourage you to run the migration recipe and see how far you get. For anything it doesn’t cover (like replacing deprecated APIs with the new alternatives), we have dedicated migration guides to help you or your AI coding agents through the rest!
Try Plus and Enterprise for free while you're at it
Plus and Enterprise editions now have a free trial. If you've just finished upgrading, you're in the perfect position to see what the solver can do with more of the engine turned on.
For example, the community edition runs on a single thread. Our enterprise version uses every core you give it, which is just a single line of configuration.
<solver>
<moveThreadCount>AUTO</moveThreadCount>
...
</solver>
On a project of any real size, you notice the difference in the first 30 seconds.
Grab a trial license at licenses.timefold.ai, the 2.0 docs walk you through setup.
A reasonable upgrade plan
Based on how our own upgrade went, you only need 5 steps:
- Get to the latest 1.x release first (we got migration recipes for that, even when coming from OptaPlanner) and run your tests.
- Make sure your build and runtime are on Java 21.
- Run the Timefold Solver 2.0 migration recipe.
- Check for lingering issues and solve them with our manual migration guides.
- Commit, ship, and kick the tires on the Enterprise Solver trial.
In case something doesn't work out for you, reach out to us on Discord or Github Discussions and we'll get you on 2.0 together.

I vibe-coded an optimization model and my colleagues found all the holes
There's a particular kind of confidence that comes right before embarrassment.
You know the one. It's the confidence of the person who watched one YouTube video about plumbing and is now absolutely certain they can replace the pipes under their sink. It's the confidence of someone who bought a guitar and is now absolutely certain they can play Dragonforce. It's the confidence, as it turns out, of someone who used AI to build an optimization model and was very pleased with himself.
That someone was me.
A brief trip back to the beginning
In one of my very first newsletters, I argued that LLMs can't solve scheduling problems, that's the domain of classical constraint solvers and metaheuristics, the "old school" AI that's been quietly doing serious work for decades. I did add that the future probably lives somewhere in the mix between old-school and new-school AI.
I still stand by that. The prediction aged well, if I say so myself.
What I didn't fully anticipate was how that mix would play out in practice. Specifically, what happens when someone (it’s me 🙂) enthusiastically dives into the GenAI side of that equation, in a domain where their expertise is... let's say, still constantly developing.
YOLO mode, activated
The experiment started simply enough. Full vibe-coding, no safety net. Just me, an AI, and some guidance files I'd thrown together. No carefully structured prompt engineering session. No domain expert on call.
And honestly? I was impressed. Genuinely. It produced something that looked coherent, spoke the right language, had variables and constraints in all the right places. So I started bragging about it on our internal Slack channel. Very proud of what I’d done, ready to tackle any optimization problem myself!
It took all of 5 minutes until the first feedback knocked that idea out of me. While I thought the model was fine, my expert colleagues found some glaring issues with it.
Now, here's the uncomfortable part: I didn't know what was missing. Not because the AI hid it. Because I didn't have enough experience in this particular area to know what to look for in the first place. The model hadn't failed visibly. It hadn't crashed. It had confidently produced something plausible and I, equally confidently, had accepted it.
Dunning-Kruger era of AI-assisted development
There's a well-documented cognitive trap (Dunning-Kruger effect) where a little knowledge creates disproportionate confidence. The less you know about a field, the harder it is to recognize what you don't know. Expertise, paradoxically, tends to make people more aware of what they're missing.
AI coding tools are, inadvertently, optimized for exactly this trap. They are extraordinarily good at making things look right, they speak the language, and produce syntactically correct, logically plausible output. In optimization, this is especially treacherous. A model with a subtle error won't necessarily blow up. It'll solve. It'll give you a number. Maybe even a reasonable-looking number. The error only becomes visible when someone who knows the domain asks: "Wait, why didn't you model this constraint?"
Now, could better inputs have helped? Probably yes. Skills files, richer context, more structured guidance could close some of that gap. But that's precisely the trap: we assume the shortcoming is in the prompt or the context, when sometimes it's the prompter.
What I actually learned
I'm not writing this to argue against using AI for development. Heck, I use it constantly and plan to keep doing so.
But the experience recalibrated something important. The real question isn't "can I build this with AI?", the answer is almost always yes, quickly and impressively. The question is: "do I understand this domain well enough to know if what I built is actually correct?"
In the first newsletter I wrote, I said the future is probably in the mix between old-school and new-school AI. I still believe that. But I'd add a third ingredient to that mix now: old-school human judgment. The kind that knows what it doesn't know.
Because the most dangerous moment isn't when the AI gets something wrong. It's when you don't realize it has.
So the next time you're feeling very good about something you built, like a nice optimization model with Timefold for example ;), find someone who knows the domain and ask them to look at it.
They might confirm you nailed it. Or they might make you realize that you still have much to learn. I speak from experience. 🙂

Timefold Solver 2.0 released
The open source release comes with additional support for complex scheduling and routing cases, a new API to escape local optima and faster solving. This release also lays the groundwork for our upcoming explainability, insights and integration functionality.
However, we’re also making some open source changes, for long term sustainability. More on that below.
New features
List variables, all grown up
Timefold Solver 1.0 already had basic support for list variables. These are typically used for vehicle routing problems (VRP) and job shop scheduling (JSS). For example: a vehicle has a list of visits. In 2.0, list variables are now fully supported across all solver features, such as:
- Pinning visits
- Allow visits to remain unassigned (no more dummy vehicles)
- Limit the value ranges per entity (reduce the search space)
This enabled us to remove chained variables: list variables are faster, easier and cleaner in every aspect.
Custom shadow variables
Some solving decisions are a result of other solving decisions. Arrival time in VRP for example. The order of the visits determines the arrival time for each visit. Such data naturally belongs in shadow variables.
Timefold Solver 1.0 had basic support for shadow variables. In 2.0, shadow variables are concise to define, straightforward to unit test, and protected from accidental loops out of the box.
Neighborhoods API to escape local optima
Out-of-the-box, Timefold Solver’s algorithms escape most local optima. However, some advanced use cases benefit greatly from custom move selection. Until now, implementing custom move selection was error-prone, verbose and limited.
Our new Neighborhoods API fixes this. It’s a clean, declarative API to define high-quality custom move selection with only a few lines of code. Similar to Constraint Streams, you can enumerate the planning entities and values, define how to sample from them, and optionally filter out combinations to only generate useful moves. Change and swap moves are available out of the box. Try it out.
Faster solving, same clean code
Since Timefold Solver 1.0, we've steadily added features that unlock meaningful performance gains. Here are a few worth knowing about.
- Precomputed joins cache the static parts of a Constraint Stream, the parts that only depend on problem facts. so the solver stops redoing the same work on every move.
- toConsecutiveSequences replaces chains of self-joins with a single collector, making one of the most common patterns in scheduling significantly cheaper.
- Shadow variable handling has been reworked to cut overhead on every variable change, which compounds across millions of moves.
- Constraint Profiling shows exactly which constraints are costing you time, so you know where to focus.
On a real-world course scheduling problem, three targeted rewrites using these features delivered a 91% speedup over a Solver version not using these features. You can learn more about this in a future blog post and during our upcoming Solver 2.0 webinar!
Smaller wins worth knowing about
- Containing joiners: Define constraints that require joining on
containing(),containedIn()orcontainingAny()logic in a far more scalable manner. For example, joining every two entities that share a tag. - Java Platform Module System (JPMS) support: Optionally run with the modulepath instead of the classpath to improve code encapsulation. Learn more.
Open source changes
We created Timefold to free the world of wasteful scheduling, not just to save OptaPlanner. Our open source solver is a critical part of that mission: Timefold Solver solves any scheduling problem, for free. We invest heavily in it, with a large, dedicated engineering team. It’s in production across the globe to schedule millions of activities every day.
As Timefold Solver grew, we expanded our offering beyond solving, into Explainability. Today, we’re restructuring the Solver editions accordingly, to better reflect how companies use it for commercial gain.
Starting with 2.0, Explainability moves to Timefold Solver Plus, a new paid edition. The score analysis and recommend assignment APIs are now part of that offering. This is a unique functionality which no other solver offers. It helps build trust and explains why a schedule looks the way it does. In the next few months, we will further expand the explainability features.
The new Plus edition gives growing teams access to more powerful features without jumping straight to Enterprise. We are offering a launch price until June 30th.
The open source edition can still solve any scheduling problem, for any constraint. It’s still free for commercial use. Still Apache License 2.0. Still fully tested, reliable and production-ready. We are expanding the solving features there too, as we’ve done for the last 20 years.
Free licenses
To support scientific research and charity organizations, Timefold Solver Enterprise (including Plus) is now free for non-profits and academic use.
Trial license
To verify the added value of Timefold Solver Plus and Enterprise, you can now easily download a free trial for 30 days.
Upgrading from 1.x
Timefold Solver 2.0 is mostly backwards compatible with 1.x. All deprecated methods have been removed to create a cleaner API.
We've worked hard to make the upgrade as smooth as possible:
- Upgrade automatically with our OpenRewrite recipe.
- If you run into any compilation error, check the comprehensive migration guide that covers every change in detail.
Timefold Solver 1.x is maintained throughout 2026 with quarterly bug-fix and security releases so you can plan your migration accordingly. That being said, we recommend switching to 2.x as soon as possible. If you run into issues while upgrading, don’t hesitate to contact us and we'll work through it together.
A note from the team
Building and maintaining a solver at this level of quality takes serious, ongoing investment. We're proud of what this community has built together, and we're committed to keeping Timefold Solver healthy for the long term.
Timefold Solver 2.0 is available now. Check out the documentation to get started today.
If you have questions, don't hesitate to reach out.

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