My Journey at Adobe as a Product Intern: Agentic AI Team
Last year, I got the offer for a Product Intern role from Adobe, and honestly, I still remember the feeling before that email came in more clearly than the day I actually got it. There’s a specific kind of restlessness that comes with waiting for a result you really want. You keep telling yourself not to check your inbox every hour, and then you check it every hour anyway. When the offer finally came, it didn’t feel like relief so much as a rush. I remember reading the mail two or three times just to be sure I wasn’t misreading anything.
The excitement got a second boost when I found out I’d be working out of the Bangalore office. I’m not going to pretend I had some deep emotional connection to the city at that point, it was more that Bangalore meant tech, meant energy, meant this was really happening and not just a nice email sitting in my inbox.
Orientation day was its own kind of overwhelming, in a good way. A lot of introductions, a lot of “so what team are you on” conversations, and that slightly nervous excitement of not knowing exactly where you’ll land. I’ll probably write a separate post going into the whole orientation and onboarding experience in more detail at some point, because it deserves its own space. For now, I want to fast forward to the part that this post is actually about: the moment I found out I’d be joining the Agentic AI team on Adobe Express, and the project that came with it.
The Project
The team handed me a problem that sounded simple in one line and turned out to be anything but: build a hybrid system, combining geometry and optimization algorithms with LLM assistance, to fix broken layouts in Adobe Express designs.
Some context on what that actually means. If you’ve ever used a design tool like Adobe Express, you know the drill. You pick a nice template, maybe a birthday flyer or a poster, and you start customizing it. You swap out the text, drop in your own photo, add one extra line because you forgot something. And somewhere in that process, the layout falls apart. Text starts overlapping. Elements drift off the edge of the canvas. The spacing that looked so clean in the original template now looks like a mess.
My job wasn’t to redesign these layouts from scratch. It was to repair them: fix the overlaps, bring things back on canvas, restore some sense of alignment, all while keeping the user’s actual content untouched.
When I first heard the brief, my brain went to the obvious place. Parse the design, ask a capable model what’s wrong with it, ask it how to fix it, apply the fix. Simple, right?
It was not simple. Not even close. And figuring out why took up most of my first week.
Why This Was Harder Than It Sounded
I spent that first few weeks barely writing any code. Instead, I was reading about constraint solvers, digging into layout algorithms, and manually breaking designs apart just to understand which failures were purely geometric and which ones were actually about meaning and intent. At the time it felt like I wasn’t making progress. Looking back, it was probably the most useful week of the whole project.
Here’s the thing about layout repair that I didn’t appreciate going in: there’s no single rulebook. A birthday invite, a resume, a promotional poster, and a social media post can all be “correct” in completely different ways. The exact same geometric arrangement can be totally intentional in one design and completely broken in another.
Think about a decorative border that’s meant to overlap a background panel. Now think about a line of body text accidentally sitting on top of that same panel. Geometrically, both look like overlaps. But only one of them is actually a problem. That difference between “looks wrong” and “is wrong” turned out to be the whole game.
It also meant this wasn’t a pure math problem. You can push elements apart, snap them to a grid, force everything to stay inside the canvas boundary, but if you don’t understand which relationships in the design actually matter, you end up fixing one issue and creating two new ones in the process.
The goal, as it got clearer over time, wasn’t to build something that fixes every layout perfectly. It was to explore how far a cost effective hybrid approach could take us: some semantic guidance where it was genuinely needed, and deterministic, rule based repair everywhere else. And to be honest about where that line should sit.
The Wrong Turns (That Taught Me the Most)
Week one, I went straight for constraint solvers. My first instinct was to treat this purely as a constraint satisfaction problem. Express “don’t overlap,” “stay inside the canvas,” and “align to this edge” as formal constraints, and let a solver spit out a clean answer.
That fell apart fast. A lot of real layout rules don’t map cleanly onto the kind of constraints classical solvers are good at. Some relationships are disjunctive, like “A has to be either left of B or above B,” and that kind of either or logic is a lot messier to express than it sounds. I didn’t throw solvers away completely, but I stopped believing they could carry the whole problem on their own.
Then I built a playground to test different algorithms. Greedy heuristics, physics based repulsion, simulated annealing, all benchmarked against randomized layouts I could reproduce and compare. Simulated annealing came out on top for pure overlap removal, and for a while I genuinely thought I’d cracked it.
Then I tried it on real Express designs, and it fell apart. On a blank canvas it was great at pushing things apart. On an actual layout, it had zero sense of what was intentional overlap versus an accident. It would happily “fix” decorative layering that was never broken in the first place. No alignment awareness either. Majority of times it removed one violation and left the design in a worse state than before.
That was the moment it really clicked for me: an algorithm can fix geometry, but it can’t tell you what the designer actually meant. I tried patching this with heuristics on top, rules telling the algorithm what to leave alone, what to protect, what it was allowed to move. That worked in some cases and completely failed in others, especially anything that needed real visual context. It taught me that heuristic optimizers are strong building blocks, but they’re fragile when you ask them to be the whole system, especially when the actual problem is about understanding structure and not just minimizing some overlap score.
Next I tried using an LLM as the planner. The idea was to let the model look at the design, figure out what was actually broken, decide what needed to move, and hand that plan to a solver to execute. I went through a lot of prompt iterations here. Single call reasoning. Splitting issue detection from repair planning. The split helped a bit, but reliability was still shaky.
Sometimes the model caught exactly the right issues. Other times it missed something obvious sitting right in front of it. Sometimes its suggested fix sounded completely reasonable in plain language but was flat out wrong once you looked at the actual coordinates. It just wasn’t deterministic enough to trust in anything close to production.
That’s when I learned something I didn’t expect to learn from the project: having the right high level architecture, semantic reasoning paired with deterministic enforcement, isn’t enough on its own. You also have to get the boundary between the two right. We didn’t want the model doing all the thinking and all the doing. We wanted the system wrapped around the model to carry most of the reliability, not the model itself.
What Actually Worked: Systems Over Prompts
The approach that finally felt promising wasn’t the one that leaned on the LLM the most. It was the one that leaned on it the least, at least for now.
Get Harsh Deep’s stories in your inbox
Join Medium for free to get updates from this writer.
What we landed on is a deterministic repair engine built as a structured pipeline:
- Classify elements — backgrounds, text, images, decorative frames, and which of these are movable versus frozen
- Detect relationships — what belongs together, what sits inside what
- Identify actual violations — because not every collision is a problem
- Repair — resize, contain, resolve overlaps, align, and clean up symmetry
- Report honestly — what moved, what’s still broken, and why
Instead of trying to solve everything in one shot, this fixes one category of issue at a time, always going for the least disruptive move that still improves the layout.
Every piece of this came directly out of what didn’t work before. Solvers taught me that we needed determinism. The annealing experiments taught me we needed some sense of intent. The LLM experiments taught me we needed guardrails around it. What we ended up with is basically the leftover wisdom from a bunch of failed attempts, put back together in a more disciplined way.
The Questions I Kept Coming Back To
Where does the model’s job end and the algorithm’s job begin? My first instinct, and I think this is common for a lot of engineers, was to try and remove ambiguity everywhere. Specify every step. Make every decision explicit. Hand the solver a fully worked out instruction set constraints derived from the model’s output.
Over time I realized that over specifying the model’s job wasn’t making the system more reliable, it was making it more brittle. The real question was never “how do I write a better prompt to fix this layout.” It was which decisions should be inferred, which should follow strict rules, and which should just be left to optimization. A lot of building this kind of system is really about deciding what you deliberately choose not to hand over to AI.
What happens when the metrics look great but the layout doesn’t? This was probably the most humbling part of the whole project. On simple, isolated cases, like one text on text overlap or one element sticking off the canvas, the system worked cleanly. Violations dropped to zero. The debug report looked perfect.
On dense clusters, where multiple elements needed to move together, the system would reduce the number of violations but not fully resolve them. At least that showed up honestly in the data.
The scarier case was different. Some layouts had decorative framing elements that were deliberately excluded from collision scoring. In those cases, the system could report a clean success while a genuinely bad looking overlap was still sitting right there, not because the code was wrong, but because the metric wasn’t measuring the thing we actually cared about. That’s when it hit me that evaluation isn’t something you bolt on at the end, it’s part of the architecture itself. A repair system without honest, falsifiable metrics will quietly lie to you.
Sequential repair versus solving everything together. The current engine repairs things in phases, which makes it easy to interpret and debug, but it can miss solutions that need overlap, alignment, and sizing to be balanced all at once. Constraint solvers optimize everything together with weighted rules. Heuristic pipelines optimize one thing at a time. Both have real strengths. If I’d had more time, the next step would have been figuring out which parts of this pipeline could actually be expressed as joint constraints and which genuinely need that greedy, step by step handling.
What Changed in How I Think
If I had to sum up the one thing I’m taking away from this, it’s that I started this project thinking AI engineering was mostly about models, and I’m ending it convinced it’s really about systems.
The model, in the places we actually used it, is just one component. Classification rules matter. Relationship detection matters. Overlap policy matters. Honest debug reporting matters. Knowing exactly where your approach breaks down matters just as much as the parts that work.
I also learned that redesigning something isn’t the same as failing at it. I rebuilt this architecture more than once. I dropped approaches I’d spent real weeks on. I simplified things that looked elegant on a design doc and completely fell apart on real layouts. Every pivot felt bad in the moment. Looking back, each one meant I understood the problem a little better than I had the week before.
Where It Actually Works
It works well on layouts with isolated, local defects, a single overlap, one element clipping off canvas, one alignment issue without other constraints fighting it. In these cases, the phased, deterministic repair holds up surprisingly well.
It struggles with dense, multi element clusters where several things need to move together at once, or with layouts built around repeating grids rather than freeform poster style composition.
And evaluation itself gets genuinely hard with designs that use intentional decorative layering, where ignoring certain elements is the right call for scoring.
That’s not a polished pitch. It’s closer to a working map, the kind you actually need if you want to improve a system instead of just showing off its best case.
If I Had Another Month
Two things I’d want to chase next. First, moving from phased heuristics toward a more hybrid, global optimization approach, translating more of the repair logic into weighted constraints that get optimized together, and keeping greedy methods only for the geometry that truly needs them. Second, using the LLM more surgically, not as the main planner, but as a layout aware tuner that helps figure out masks, frozen regions, and parameter choices for specific design types, instead of trying to reason about full coordinates in one shot.
I don’t think the answer is “more AI” or “less AI.” I think it’s about placing AI more carefully inside a system that already knows how to be deterministic where it counts.
Final Thoughts
When I look back at this project, I don’t really remember it through the prototypes or the benchmark numbers. I remember realizing that layout repair is a systems problem as well as geometry problem. I remember that the most impressive version of this system was never the one that fixed everything, it was the one that knew what it couldn’t fix and could tell you why. And I remember that most of the real engineering work happened before and after the actual model call, not during it.
More than anything, this project changed how I define progress. It’s not finding the one algorithm that solves layout repair. It’s building an honest mental model of the problem itself: its ambiguity, its failure modes, its real constraints, and its ceiling.
The biggest thing I built here wasn’t a pipeline. It was a way of thinking about hybrid AI systems: where to trust inference, where to enforce rules, how to measure success without fooling yourself, and how to keep redesigning things without treating every pivot as wasted effort.
I’m genuinely grateful to my mentor (Amish Bedi) and the team for giving me the room to explore, fail out loud, and iterate, especially on a problem that looks deceptively simple until you actually try to solve it for real designs.
This project was bounded in scope, but I have a feeling the lessons from it are going to stick around a lot longer than the internship did.




