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.