Software automation is good at turning instructions into actions.
A pipeline can deploy a release, Terraform can provision infrastructure, a runbook can restart a service, or an agent can inspect telemetry and choose a remediation step.
But all of those systems share the same weakness: they're usually better at executing instructions than at representing the intent behind those instructions.
That gap is easy to ignore while automation is simple. If a script only runs one command, the script itself may be enough to explain what should happen.
But as systems become more distributed, more policy-driven, and more autonomous, execution logic starts carrying responsibilities it was never designed to own.
A deployment workflow may now encode:
what should be deployed where it may run which constraints must hold which evidence must be checked who may approve the change when rollback is required which failures are acceptable At that point, the automation is no longer just executing. It's also becoming the place where operational intent is defined.
And that's a problem.
Because when intent and execution are fused together, changing the executor can also change the meaning of the operation.
In my previous article, I introduced the idea of an executable operational specification: a machine-readable description of what an operation is supposed to achieve and how its outcome can be evaluated.
In this article, I want to go one level deeper.
The central question is:
What should exist between human intent and the system that executes it?
I will explore:
why automation tools often become accidental specifications
why intent and execution evolve at different speeds
what gets lost when intent is embedded in pipelines and scripts
why desired state is useful but still incomplete
how to introduce an explicit operational intent layer
how preconditions, constraints, evidence, and recovery rules fit into that layer
how execution plans differ from operational specifications
how this structure helps with AI agents
and what this separation still can't solve
The goal isn't to add another abstraction for its own sake. The goal is to make operational decisions easier to understand, evaluate, and change without rewriting the meaning of the operation every time the execution mechanism changes.
Prerequisites
You should be comfortable with:
key software architecture concepts
CI/CD
infrastructure automation
observability
distributed systems
basic policy concepts
TypeScript or a similar language
You don't need a specific orchestration tool or cloud platform. The examples are deliberately small and tool-independent.
Table of Contents
Automation Tools Often Become Accidental Specifications
Consider a deployment workflow. It may begin as something simple:
steps: - build - test - deploy Then production requirements appear.
You add:
approval region validation health checks canary percentage rollback logic error-rate thresholds latency thresholds artifact verification Eventually, the pipeline becomes the only place where the operation is fully described.
That creates an accidental architecture:
Operational intent ↓ pipeline configuration ↓ execution The pipeline is now doing two jobs: it describes what the operation means and how the operation is executed.
Those responsibilities are related, but they're not identical.
Suppose this workflow says:
deploy: image: orders:v42 verify: errorRateBelow: 0.01 rollback: whenVerificationFails: true That may work perfectly. But now imagine moving the deployment to another platform.
If the operational rules only exist inside this pipeline, you must rediscover and reimplement them while moving the executor.
The real migration becomes:
move execution mechanism + reconstruct operational intent That's much more dangerous than just changing tooling.
Intent and Execution Change at Different Speeds
Operational intent is often relatively stable.
For example:
Deploy the approved version. Maintain availability. Do not exceed the error-rate threshold. Keep rollback possible. Do not deploy outside the approved region. Those expectations may remain valid for years. But execution mechanisms change much more frequently.
A team may move through:
shell scripts Jenkins GitHub Actions Argo CD Kubernetes operators cloud-native deployment services AI agents If intent is tightly coupled to those mechanisms, every tooling change risks becoming an operational redesign.
That creates unnecessary churn.
You want:
stable intent ↓ replaceable execution not:
new executor ↓ rediscover the meaning of the operation This separation becomes more important as automation becomes more sophisticated.
The more intelligence the executor contains, the easier it becomes for intent to disappear inside implementation details.
What Gets Lost When Intent Lives Inside the Executor
When operational intent is embedded directly in scripts and pipelines, several things become harder.
1. Review
A reviewer may see:
if: error_rate < 0.01 but not know whether that threshold is:
a business requirement an SRE policy a temporary rollout rule a historical workaround The value is executable. Its meaning isn't explicit.
2. Auditability
Six months later, somebody asks:
Why was this deployment allowed?
The answer may require reconstructing:
pipeline version feature flags runtime variables dashboards approvals operator decisions 3. Portability
Changing tools means reimplementing rules.
4. Testing
You can test whether the pipeline works. But it's harder to test whether the operational intent itself is complete.
5. Governance
Authorization rules may become buried in workflow code.
For example:
production changes require approval may be implemented as a platform-specific branch protection rule instead of being represented as part of the operation.
6. AI Execution
An AI agent may generate a valid sequence of commands while violating an unstated operational constraint.
The problem isn't that the executor is weak. It's that the intent isn't independently represented.
Desired State Helps, But It's Not the Whole Intent
Many modern systems already use desired-state models.
For example:
replicas: 3 image: orders:v42 That's useful.
The system can compare:
desired state vs. observed state and reconcile the difference.
But operational intent often includes more than final configuration.
Suppose the desired state is:
orders:v42 replicas: 3 The operation may also require:
do not reduce availability below 99.9% do not deploy if payment dependency is degraded keep error rate below 1% require approval for production rollback if latency exceeds 400 ms Those aren't simply target-state properties.
Some are:
preconditions runtime constraints evidence requirements governance rules recovery rules Desired state is part of operational intent, but it's not always the entire operational intent.
The Missing Layer
A more explicit architecture looks like this:
Human / organizational intent ↓ Operational specification ↓ Execution planning / binding ↓ Executor ↓ Actions ↓ Evidence ↓ Evaluation The operational specification is the missing layer.
Its job is not to execute commands. Its job is to preserve the meaning of the operation independently from the mechanism performing it.
That layer can contain:
objective scope preconditions constraints required evidence authorization rules recovery conditions The executor then answers a different question:
How can I satisfy this specification using the tools available to me?
That's a cleaner separation.
A Small Deployment Example
Suppose we want to deploy version v42 of the Orders service.
The operational objective is:
Deploy Orders v42 to production. But that's not enough.
We also know:
the payment dependency must be healthy at least 3 replicas must remain available error rate must remain below 1% p95 latency must remain below 400 ms production deployment requires approval rollback must remain possible We can represent those concerns separately.
type DeploymentIntent = { service: string; version: string; environment: string; preconditions: { paymentDependencyHealthy: boolean; approved: boolean; }; constraints: { minAvailableReplicas: number; maxErrorRate: number; maxP95LatencyMs: number; }; recovery: { rollbackOnViolation: boolean; }; }; Then:
const intent: DeploymentIntent = { service: "orders", version: "v42", environment: "production", preconditions: { paymentDependencyHealthy: true, approved: true, }, constraints: { minAvailableReplicas: 3, maxErrorRate: 0.01, maxP95LatencyMs: 400, }, recovery: { rollbackOnViolation: true, }, }; Notice what's missing. There's no:
kubectl Terraform GitHub Actions Argo cloud API The intent doesn't yet care how the deployment happens.
That's deliberate.
Separate Preconditions from Execution Steps
A precondition answers:
Is this operation allowed to start?
That's different from:
Which step should run first?
For example:
payment dependency must be healthy production approval must exist artifact must be signed maintenance window must be open These aren't execution instructions. They're conditions that should be true before execution begins.
A simple evaluator might look like this:
type Preconditions = { paymentDependencyHealthy: boolean; approved: boolean; }; function canStart( preconditions: Preconditions ): boolean { return ( preconditions.paymentDependencyHealthy && preconditions.approved ); } If:
canStart({ paymentDependencyHealthy: false, approved: true, }); returns:
false the operation shouldn't begin.
That decision shouldn't depend on whether the executor is Kubernetes, a script, or an AI agent. The precondition belongs to the operational intent.
Separate Constraints from the Mechanism
Constraints answer what must remain true while or after this operation runs.
Examples:
replicas >= 3 error rate <= 1% latency <= 400 ms cost increase <= 20% region must remain us-east-1 An executor may use many different strategies to satisfy them.
For example, to maintain three replicas:
Kubernetes may use a rolling deployment another platform may use blue/green deployment an agent may temporarily scale capacity before switching traffic The mechanism differs, but the constraint does not.
That's exactly why the constraint should live outside the executor.
Make Evidence Requirements Explicit
A constraint is only useful if you can evaluate it.
Suppose you define:
error rate <= 1% Then you need a source of evidence.
For example:
Prometheus Datadog CloudWatch OpenTelemetry custom application metrics The specification shouldn't necessarily hard-code one vendor. But it should say that evidence is required.
For example:
type EvidenceRequirement = { name: string; metric: string; }; const evidenceRequirements: EvidenceRequirement[] = [ { name: "availability", metric: "availableReplicas", }, { name: "errors", metric: "errorRate", }, { name: "latency", metric: "p95LatencyMs", }, ]; The executor or evidence collector can decide where those values come from. The specification defines what must be known. That distinction is important.
Define Recovery Before Execution Starts
Many operational workflows define rollback only after something goes wrong. But that's too late. Recovery is part of operational intent.
For example:
If error rate exceeds 1%, stop rollout. If p95 latency exceeds 400 ms, restore previous version. If evidence is unavailable, require human review. These aren't just implementation details. They express acceptable operational behavior.
A simple representation might be:
type RecoveryPolicy = { rollbackOnConstraintViolation: boolean; requireHumanReviewWhenEvidenceMissing: boolean; }; The executor can implement rollback differently. The policy still belongs to the operation.
Operational Specification vs Execution Plan
This distinction is important.
An operational specification answers:
What should happen? What must be true? What evidence is required? What happens if constraints fail? An execution plan answers:
Which actions will we perform? In what order? Using which tools? For example:
Specification
Deploy Orders v42. Keep at least 3 replicas available. Error rate <= 1%. Rollback if the constraint is violated. Execution plan
1. Scale Orders to 6 replicas. 2. Deploy v42 to 1 replica. 3. Route 10% of traffic. 4. Wait 5 minutes. 5. Check metrics. 6. Increase traffic. 7. Remove old replicas. The plan is one possible way to satisfy the specification.
Another executor might produce a different plan. That's useful. It means the same operational intent can survive changes in tooling and execution strategy.
Why This Matters for AI Agents
This separation becomes especially important when the executor is an AI agent.
A traditional script usually has a predictable path while an agent may decide dynamically.
For the same incident, it might choose:
restart scale reroute disable feature change configuration If correctness is defined by following one exact sequence, dynamic execution becomes difficult to verify.
But if correctness is defined by an operational specification, the agent has flexibility.
For example:
Objective: restore checkout availability Preconditions: payment database reachable Constraints: authentication remains enabled no payment duplication error rate <= 1% cost increase <= 20% Evidence: checkout success rate duplicate payment count error rate cost delta Recovery: escalate if constraints cannot be satisfied The agent can choose different actions, but its behavior is still bounded.
That gives us:
Intent ↓ Specification ↓ Agent-generated plan ↓ Execution ↓ Evidence ↓ Evaluation The agent is free to decide how. It's not free to redefine what success means.
That distinction will become increasingly important as autonomous systems gain more operational authority.
A Minimal TypeScript Model
We can combine these ideas into a small generic model.
type OperationalSpec<TContext, TEvidence> = { name: string; canStart( context: TContext ): boolean; evaluate( evidence: TEvidence ): EvaluationResult; recovery: RecoveryPolicy; }; type EvaluationResult = { conformant: boolean; failures: string[]; }; type RecoveryPolicy = { rollbackOnConstraintViolation: boolean; }; For a deployment:
type DeploymentContext = { paymentDependencyHealthy: boolean; approved: boolean; }; type DeploymentEvidence = { version: string; replicas: number; errorRate: number; p95LatencyMs: number; }; Then:
const deploymentSpec: OperationalSpec< DeploymentContext, DeploymentEvidence > = { name: "deploy-orders-v42", canStart: (context) => context.paymentDependencyHealthy && context.approved, evaluate: (evidence) => { const failures: string[] = []; if (evidence.version !== "v42") { failures.push( "wrong-version" ); } if (evidence.replicas < 3) { failures.push( "insufficient-replicas" ); } if (evidence.errorRate > 0.01) { failures.push( "error-rate-too-high" ); } if ( evidence.p95LatencyMs > 400 ) { failures.push( "latency-too-high" ); } return { conformant: failures.length === 0, failures, }; }, recovery: { rollbackOnConstraintViolation: true, }, }; Now execution can be handled elsewhere.
For example:
interface DeploymentExecutor { execute( spec: typeof deploymentSpec ): Promise<DeploymentEvidence>; } That executor could be backed by:
Kubernetes a cloud platform a test harness a local simulator an AI agent The specification remains the reference point.
A Practical Workflow
If you want to introduce this separation into an existing automation system, start with one operation.
1. Pick a Repeated Operation
For example:
deploy service rotate certificate scale worker pool restore backup 2. Extract the Objective
Write down what the operation is trying to achieve. Avoid commands.
3. Extract Preconditions
Ask:
What must already be true before this operation can begin?
4. Extract Constraints
Ask:
What must remain true during or after execution?
5. Define Evidence
For every constraint, identify the observation required to evaluate it.
6. Define Recovery
Decide what should happen when evidence shows that a constraint was violated.
7. Leave Execution Separate
Let the current automation tool remain the executor. Don't rewrite everything at once.
8. Evaluate the Existing Executor
Ask whether the current script, pipeline, or agent satisfies the extracted specification.
This is useful because the separation can be introduced incrementally. You don't need a new operations platform to begin.
What This Separation Doesn't Solve
Separating intent from execution doesn't automatically make operations correct.
You can still have:
wrong objectives bad thresholds missing evidence misleading metrics conflicting constraints unsafe executors security bugs incorrect recovery policies The specification itself can be wrong. That matters.
A formal representation doesn't make an assumption true. It only makes the assumption easier to inspect and evaluate.
There's also a cost. More explicit operational intent means more artifacts to maintain.
If specifications aren't versioned and reviewed, they can become stale. If evidence definitions are weak, conformance results can become misleading. And some operational decisions remain difficult to formalize.
Human judgment doesn't disappear. The value comes from making more of that judgment explicit.
From Automation Logic to Operational Contracts
There's another way to think about this separation.
A mature operational specification starts to behave like a contract.
It says:
This operation may begin under these conditions. It must preserve these constraints. It must produce this evidence. Its outcome will be evaluated using these rules. If it fails, these recovery conditions apply. That's much stronger than:
run this workflow The workflow is an implementation. The operational contract describes what that implementation is responsible for achieving.
This gives us a structure like:
Intent ↓ Operational contract ↓ Execution plan ↓ Executor ↓ Evidence ↓ Evaluation That separation creates room for something important:
multiple execution strategies for the same operational intent And once that's possible, another question appears:
Can two different executors satisfy the same operational specification?
That's the question I want to explore next.
Conclusion
Automation has made software operations faster and more repeatable.
But execution is only one part of an operation. We still need to know:
why the operation is allowed what outcome is intended what constraints must hold what evidence is required what happens when the constraints fail When all of that logic lives inside a pipeline, script, or agent, the executor becomes the accidental owner of operational meaning.
That coupling makes systems harder to review, migrate, audit, and verify.
A better separation is:
Intent ↓ Operational specification ↓ Execution plan ↓ Executor ↓ Evidence ↓ Evaluation The specification describes what success means. The executor decides how to achieve it.
That distinction becomes increasingly valuable as execution mechanisms become more dynamic. Especially when those executors are AI agents.
Because the more freedom we give a system to decide how work gets done, the more explicit we need to be about what the work is supposed to achieve.