Good Agentic Workflows Are Born From Pain, Not Principles

A Web Developer based in Belgium, specialized in PHP & JS development
Over the past few weeks, I have been experimenting with a more agentic way of building software projects.
I started with a project template that Sol helped me generate. It seemed to cover all the typical needs. Then I started working, watched what happened, and adjusted the environment along the way.
An agent would do something inefficient, misunderstand an architectural boundary, run far too many checks, follow a lint rule too literally, or introduce an abstraction that was technically clean but not particularly useful.
I would look at why it happened and decide where the fix belonged. Sometimes the answer was a better instruction. Sometimes it was a test, a script, an architecture check or a change to CI. And quite often, the answer was simply to remove or simplify something (because agents LOVE to write MORE STUFF).
That experience led me to a phrase I keep coming back to:
Good agentic workflows are born from pain, not from principles.
Not because principles do not matter, but because it is difficult to know in advance which ones will actually matter for a particular project. It is even harder to phrase them well before encountering the situations where they apply.
Agents are very good at finding friction
An agentic project is still a normal software project.
Clear architecture, fast tests, predictable commands, good names and reproducible environments all help. Agents benefit from those things for the same reasons humans do, but they also expose their absence very quickly.
A developer may remember that a particular command needs PostgreSQL running, that one tool behaves differently on Windows, or that a certain abstraction should not be used in one part of the application. An agent tends to discover those things from scratch.
Initially, the obvious response is to add another instruction: “Before doing X, remember to do Y.”
I still do that when it makes sense, but quite often the more interesting question is why Y was difficult to discover in the first place. If several agents get confused by the same thing, perhaps the project itself is confusing.
This has made agent behaviour surprisingly useful as a form of continuous developer-experience testing.
Let rules earn their place
It is very easy to start an agentic project with too many rules.
There is no shortage of reasonable software advice: keep methods small, limit parameters, abstract dependencies, add tests, isolate infrastructure, document decisions.
The problem is not that these ideas are wrong. The problem is applying them without context.
I encountered a simple example with a static-analysis rule about parameter counts. Too many parameters can certainly indicate a design problem. But if an agent responds by wrapping perfectly understandable named parameters into a meaningless Options object purely to make the warning disappear, the code has not improved.
It has simply learned to satisfy the metric.
That led me to change the rule rather than the code.
Now the metric is treated as a review signal. If it reveals a genuine cohesion problem, I refactor. If the existing shape is clearer, I keep it and make the exception explicit.
That distinction sounds small, but I think it captures an important part of working with agents. They are very good at following rules, which makes the quality and nuance of those rules unusually important.
The useful part of a rule is often the exception
My project instructions have grown quite a bit, but many of the rules I find most useful are not simple “always” or “never” statements. They describe a boundary.
For example, one project I am working on is currently Belgium-first. It therefore uses Europe/Brussels as its civil timezone and stores instants properly in UTC.
But the rule does not continue with “therefore build a generic timezone abstraction”.
Quite the opposite.
There is no TimezoneContext, per-user timezone machinery or generic timezone layer until the product actually needs one. If the product eventually operates across several timezones, then timezone becomes a real domain concept and the model can evolve.
The current rule solves the current problem without trying to solve every future version of it.
I have similar distinctions elsewhere. Deferred work and durable work are not the same thing.
An in-memory adapter is useful when persistence is irrelevant, but it should not start reimplementing locking, transactions, database constraints and PostgreSQL behaviour.
A worktree is useful for concurrent writers, but adding one for every agent session also carries a cost (I mean, worktrees are doable, but it's not quite as easy as “let's just use worktrees”).
These are all rules. The useful part is often knowing where they stop.
Prefer executable rules when possible
Once I understand a recurring problem, I try not to rely on prose if tooling can enforce it more reliably.
Formatting belongs in the formatter. Architecture boundaries are better expressed through architecture checks. Database guarantees belong in PostgreSQL when appropriate. A bug that happened once deserves a regression test if it is likely to happen again.
The instructions can then focus on why the constraint exists and where judgment is still required.
This is much more useful than asking an agent to remember a long list of conventions. “Please respect the architecture” is vague; an architecture check that fails on the wrong dependency gives both the agent and the human something concrete to work with.
Give the project a good interface
The commands exposed by the repository are effectively part of the interface agents work with.
I have found that things improve considerably when common intents have obvious commands. An agent should not have to reconstruct how PHPUnit, Mago, PHPStan, Playwright, Doctrine, Bun and Docker fit together every time it changes a few files.
There should be clear entry points for common tasks (and make is your friend here to unify all your tooling needs).
I also try to make those the same commands developers use. I am not particularly interested in creating a parallel development environment designed only for agents. If making something easier for an agent also makes onboarding a human developer easier, that is usually a healthier improvement.
An interesting side effect is that agent mistakes sometimes reveal bad interfaces.
I have had agents guess command-line options for a project-specific CLI based on their knowledge of other tools. The fix was not “agents should be smarter”.
Agents can have very different levels of autonomy and capability. Astra/Fable is not DeepSeek Flash, obviously. Knowledge cutoffs do not help either, so relying on the agent's “base knowledge” to understand a local tool is not a particularly robust contract.
The better fix was to provide one canonical way to discover and run those commands.
Reduce noise instead of adding ceremony
Agents are quite happy to run every available check after every edit.
At first that feels reassuring. It quickly becomes expensive and noisy.
I had CI running PHP checks, frontend checks and browser tests for changes that only modified documentation. Nothing was technically wrong with that setup, but it provided very little useful information.
So I moved toward proportional validation.
During implementation, I want the cheapest check that can actually disprove the current change. In CI, jobs should run when the changed surface can plausibly invalidate what they test. Larger gates still have their place, but not every edit needs to pay for all of them.
There is a strong temptation in agentic workflows to equate more validation with more safety. In reality, some of it is simply wasted time and compute effort.
If a supposedly fast check slowly grows until every small change becomes noticeable, people and agents start batching more work together. Feedback arrives later and changes become harder to review. The tooling is technically more thorough while the overall workflow has become worse.
Do not reproduce production inside your test doubles
Testing gave me another good example of why simple rules are rarely enough.
At one point, I had to decide whether to favour in-memory implementations or simply use PostgreSQL for more tests. There is no particularly useful universal answer.
For a pure application flow where persistence semantics do not matter, an in-memory adapter can make the test extremely fast and focused.
But the fake has to remain a fake.
Once it starts reproducing transaction behaviour, overlap constraints, database ordering, locking or persistence coordination, I am effectively implementing another database badly. At that point the simpler solution is often to test against PostgreSQL.
The distinction I ended up with is semantic rather than technological: use the real database when the assertion depends on database behaviour, not merely because the production application happens to have a database.
That rule came out of an actual disagreement with the code and is much more useful than either “unit tests should never touch the database” or “always test against the real stack”.
Agents make unnecessary code very affordable
This may be the biggest change for me.
Agents can write a lot of code very cheaply. An interface, a registry, a couple of adapters, a configuration object and all the associated tests are not a large amount of effort anymore.
That means implementation cost provides much less resistance to over-engineering than it used to.
The dangerous code is not necessarily bad code. It can be well structured, documented, fully tested and completely unnecessary.
As a result, I find myself asking whether a piece of code needs to exist much more often than before.
I now have an explicit maintenance-first bias. I prefer extending an existing abstraction when it fits, and I introduce a new one when there is a real boundary, repeated need or clear improvement in the model. I try not to add infrastructure simply because it is common in modern architectures.
The same reasoning applies to technology choices. I am happy to use PostgreSQL when I need what PostgreSQL provides, while a simple framework queue may be enough until there is evidence that a dedicated broker is needed. Redis, Kafka, Kubernetes, microservices or a CQRS framework are not bad technologies; they simply do not make a project better by being present.
With agents making implementation so cheap, the ability to say “this is enough” becomes surprisingly important.
Audit the rules as well as the code
Reviewing agent output is obvious. Reviewing the instructions given to the agents is just as important.
A project gradually accumulates rules. Each one usually has a reasonable history behind it, but their combination can become awkward over time.
One guideline encourages an abstraction. Another discourages large constructors. A static-analysis rule complains about the resulting object. The agent creates yet another object to satisfy that rule. At some point the project is solving problems created mostly by its own process.
So I occasionally audit the workflow itself.
I look at whether a rule is still useful, whether it describes a real invariant or merely a preference, whether tooling could enforce it better, and whether the growing list of exceptions is actually telling me that the original rule was badly phrased.
Sometimes removing an instruction makes the agent behave better.
The same applies to skills.
A generic Symfony or PostgreSQL skill can provide excellent background knowledge, but it does not know the history of a particular project or the trade-offs already made there. I have had much better results adapting skills to the project than adapting the project to generic skills.
Watch the path, not only the diff
A correct final diff can hide a poor process.
An agent may eventually solve an issue after inspecting lots of irrelevant files, rerunning expensive checks, guessing commands or introducing temporary infrastructure it did not need.
I think those behaviours are worth noticing because they often point at friction in the project rather than a deficiency in the agent itself.
If several agents search for the same information, that information may simply be too difficult to find. If they repeatedly guess the same command incorrectly, the command interface may need improvement. If they keep introducing the same abstraction, perhaps an architectural decision has never been made explicit.
This is also why I keep normal engineering artifacts around agent work: issues, commits, branches, pull requests when appropriate, tests and architecture decisions.
I do not want to supervise every keystroke. I want the work to remain understandable afterward, including the reasons behind it.
Avoid process for the sake of process
Perhaps my favourite rule in my current AGENTS.md is also one of the least technical ones: procedures are a tool, not an end.
If a repository rule is ineffective, imprecise or slows work without providing enough value, the agent is expected to point that out rather than blindly following it.
I think this matters because an agentic workflow otherwise has a natural tendency to grow indefinitely. Every incident produces another instruction, another check or another precaution, while very little ever gets removed.
Given enough time, the workflow can turn into exactly the kind of bureaucracy it was supposed to avoid (and I hate bureaucracy, for real).
So I want the feedback loop to work in both directions. The project gives the agent boundaries and feedback, but the way agents behave also reveals when those boundaries are unclear, redundant or counterproductive.
The human still decides which trade-offs to make, but the rules themselves should remain open to revision.
That also keeps AGENTS.md from becoming a constitution. I see it more as accumulated operational knowledge: what has gone wrong before, what currently matters, and what seems to be the cheapest reliable way of avoiding the same problems again.
Let the workflow emerge
My current setup is considerably more detailed than when I started, but very little of that detail was designed upfront.
Formatting became automatic after formatting created pointless iterations. Validation became proportional after broad checks produced more noise than useful feedback. Static-analysis metrics became review signals after agents started optimizing the code for the metric rather than for readability.
The boundary between in-memory tests and PostgreSQL became clearer only after fake persistence started becoming too clever. Ownership and worktree rules appeared after concurrency between agents created real friction. Some rules also disappeared or were rewritten once I understood the problem better.
That history is why I am skeptical of universal agentic-development setups.
Different projects have different risks, architectures, tools and histories. Even two technically similar projects may need different rules because they have encountered different problems.
What has worked well for me is to let the workflow emerge from those problems rather than trying to predict all of them upfront.
When friction appears, I first try to understand where it actually comes from. Sometimes the right fix is in the code, sometimes in the architecture, sometimes in a test or a tool, and occasionally it really is another sentence in AGENTS.md.
The important part is not to automatically turn every incident into more process.
Over time, this creates something much more specific to the project than a generic agentic template could provide. The rules have history behind them, the exceptions tend to explain the boundaries, and the tooling carries as much of the repetitive work as possible.
I do not think the goal is to build an impressive environment for coding agents. I want a project that remains understandable, maintainable and pleasant to change, regardless of whether the next contribution comes from me or from an agent.
Agents are simply very effective at revealing the places where that is not yet true.
And for now, that has been one of the most useful parts of working with them.



