Home › DevOps › Control-M Ended Not OK: Find the Real Failure and Rerun Safely
Intermediate 3 min · October 30, 2026

Control-M Ended Not OK: Find the Real Failure and Rerun Safely

Control-M Ended Not OK? Trace the real failure from job log to sysout, then rerun safely without duplicating data or breaking dependencies.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
October 31, 2026
last updated
2,099
articles · all by Naren
Before you start⏱ 22 min
  • ✓Basic Control-M job scheduling concepts (jobs, folders, dependencies)
  • ✓Access to Control-M Monitoring and the job's log/sysout
  • ✓Familiarity with your shell or scripting language used in job commands
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
⚡Quick Answer

To find the real failure, open the job's log in Control-M Monitoring, check the sysout for the exact error, and verify any post-processing conditions. To rerun safely, first confirm idempotency, clear the failure condition, and use 'Rerun' from the job's context menu—only rerun if you know the job is safe to repeat.

✦ Definition~90s read
What is Control-M Ended Not OK?

Control-M Ended Not OK is the status assigned when a job's exit code is non-zero or a post-processing condition fails. It is not a root cause—it is a signal that the job's command, script, or downstream check returned a failure. The real failure lives in the job log, sysout, or a failed condition, and rerunning without diagnosing it is how you turn a transient blip into a data corruption incident.

★
Think of Control-M as a restaurant kitchen expediter.
Plain-English First

Think of Control-M as a restaurant kitchen expediter. When a dish comes back to the pass marked 'not OK', the expediter doesn't just shout 'cook it again'—they check whether the steak was raw, the sauce was cold, or the plate was wrong. If you rerun without reading the ticket, you might send out the same raw steak, or worse, double-charge the customer. The 'Ended Not OK' is the ticket; the real failure is why the dish failed.

Most Control-M operators treat 'Ended Not OK' as a button to press, not a symptom to read. That habit is how a one-minute network blip becomes a three-hour data reconciliation. The status tells you a job failed; it does not tell you why, and rerunning blindly is how you duplicate payments, double-send emails, or corrupt a downstream table. The real failure is always in the log, the sysout, or a condition—never in the status itself. After this article, you'll be able to trace any Ended Not OK to its root cause in under five minutes, decide whether a rerun is safe, and execute the rerun without breaking dependencies or duplicating side effects.

Why 'Ended Not OK' Is a Symptom, Not a Diagnosis

Control-M sets Ended Not OK when a job's command returns a non-zero exit code, a post-processing condition fails, or the output matches an On/Do rule that sets the status to NOTOK. That status is a signal, not a root cause. The real failure is always in one of three places: the job's sysout (application output), the Control-M log (scheduler-level errors), or a condition that never arrived. Treating the status as the problem is like treating a fever as the disease—you'll keep giving aspirin while the infection spreads. The first rule of production triage: open the sysout before you touch the rerun button.

inspect_job_failure.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Simulate a job that fails after partial work, then inspect its output.

JOB_LOG="/tmp/controlm_job_$$.log"

# Simulate a job that writes a file then fails on a permission check
echo "Starting data export..." > "$JOB_LOG"
echo "Wrote 500 rows to /data/export.csv" >> "$JOB_LOG"
# This line fails because /root is not writable by the job user
touch /root/export.done 2>> "$JOB_LOG"
EXIT_CODE=$?

echo "Job exited with code $EXIT_CODE" >> "$JOB_LOG"

# The operator sees 'Ended Not OK' but the real error is here:
echo "--- Job output ---"
cat "$JOB_LOG"

# Cleanup
rm -f "$JOB_LOG"
Output
--- Job output ---
Starting data export...
Wrote 500 rows to /data/export.csv
touch: cannot touch '/root/export.done': Permission denied
Job exited with code 1
⚠ Production Trap:
If you rerun this job, it will write another 500 rows to /data/export.csv because the export step already succeeded. The failure was only in the completion marker. Always check whether the job's main work completed before rerunning.

Reading the Sysout: Where the Real Error Lives

Control-M's job log shows scheduler events—when the job started, which node it ran on, and the exit code. The sysout (View Output) contains what your script actually printed. That's where you'll find the stack trace, the SQL error, or the 'Permission denied'. The classic mistake is staring at the Control-M log and seeing only 'Command failed with exit code 1'—that's the scheduler telling you the script failed, not why. Always open the sysout first. If the sysout is empty, the job failed before producing output—check the command path, environment variables, and user permissions.

sysout_vs_ctlmlog.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Demonstrate the difference between Control-M log and job sysout.

# Simulate a job that fails with a database connection error
cat > /tmp/db_job.sh <<'EOF'
#!/bin/bash
echo "Connecting to database..."
# Simulate a connection failure
echo "ERROR: ORA-12170: TNS:Connect timeout occurred" >&2
exit 1
EOF
chmod +x /tmp/db_job.sh

# Run the job and capture both stdout and stderr
/tmp/db_job.sh > /tmp/job_sysout.log 2>&1
EXIT_CODE=$?

echo "Control-M log would show: Job ended with exit code $EXIT_CODE"
echo "Sysout contains the real error:"
cat /tmp/job_sysout.log

rm -f /tmp/db_job.sh /tmp/job_sysout.log
Output
Control-M log would show: Job ended with exit code 1
Sysout contains the real error:
Connecting to database...
ERROR: ORA-12170: TNS:Connect timeout occurred
💡Senior Shortcut:
In Control-M Monitoring, right-click the job and choose 'View Output' (or press Ctrl+O). If the sysout is huge, use the search box for 'ERROR', 'Exception', or 'failed'. The first match is usually the root cause.

Conditions: The Silent Failure Mode

Not all Ended Not OK statuses come from a non-zero exit code. A job can fail because a condition it waited for never arrived, or because a post-processing condition check failed. These are silent—the sysout may show success, but Control-M marks the job Not OK because the expected condition wasn't set. The fix is to trace the condition chain: which upstream job should have set it, did that job run, and did it set the condition with the right date? Use ctmcontb to list active conditions and their timestamps. If the condition is missing, rerunning the downstream job won't help—you need to fix the upstream job first.

condition_trace.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Simulate tracing a missing condition with ctmcontb (conceptual).

# In real Control-M, you'd run:
# ctmcontb -LIST CONDITION_NAME

# Here's a mock output showing a condition that was never set:
echo "Condition: DAILY_SALES_LOADED"
echo "Status: Not found"
echo "Expected by job: SALES_REPORT (downstream)"
echo "Upstream job: SALES_LOAD (should set condition)"
echo "Upstream job status: Ended Not OK at 01:45"

echo ""
echo "Diagnosis: Upstream job failed, so condition was never set."
echo "Fix: Rerun SALES_LOAD first, then SALES_REPORT."
Output
Condition: DAILY_SALES_LOADED
Status: Not found
Expected by job: SALES_REPORT (downstream)
Upstream job: SALES_LOAD (should set condition)
Upstream job status: Ended Not OK at 01:45
Diagnosis: Upstream job failed, so condition was never set.
Fix: Rerun SALES_LOAD first, then SALES_REPORT.
⚠ Never Do This:
Rerunning the downstream job when the upstream condition is missing will just fail again. Always fix the upstream job first, then rerun downstream.

Idempotency: The Rerun Safety Check

Before you rerun any job, ask one question: if this job runs twice, will it cause harm? If the answer is yes, you cannot safely rerun without manual cleanup. Idempotent jobs can be rerun freely—they check for existing work and skip or overwrite safely. Non-idempotent jobs (payments, emails, inserts without deduplication) require a pre-rerun check: query the target system for evidence of the first run. If the job wrote data, you must either roll back or make the job idempotent before rerunning. The safest pattern is to build idempotency into every job from day one: use unique keys, check for existing records, and make writes upserts.

idempotent_payment.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# io.thecodeforge — DevOps tutorial
# Idempotent payment job: safe to rerun without duplicating transfers.

import sqlite3
from datetime import date

DB_PATH = "/tmp/payments.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS transfers (
            idempotency_key TEXT PRIMARY KEY,
            employee_id TEXT,
            amount REAL,
            transfer_date TEXT
        )
    """)
    conn.commit()
    return conn

def transfer_payment(conn, employee_id, amount):
    # Build a deterministic key: employee + date + amount
    key = f"{employee_id}-{date.today().isoformat()}-{amount}"
    try:
        conn.execute(
            "INSERT INTO transfers (idempotency_key, employee_id, amount, transfer_date) VALUES (?, ?, ?, ?)",
            (key, employee_id, amount, date.today().isoformat())
        )
        conn.commit()
        print(f"Transferred {amount} to {employee_id}")
    except sqlite3.IntegrityError:
        # Key already exists: this is a rerun, skip safely
        print(f"Skipping {employee_id}: already transferred today")

if __name__ == "__main__":
    conn = init_db()
    # First run
    transfer_payment(conn, "EMP001", 5000.0)
    # Simulate rerun
    transfer_payment(conn, "EMP001", 5000.0)
    conn.close()
Output
Transferred 5000.0 to EMP001
Skipping EMP001: already transferred today
💡Senior Shortcut:
Use a unique idempotency key that includes the job's run date and a business identifier. Store it in the target system with a unique constraint. On rerun, the insert fails gracefully and you skip the side effect.

The Safe Rerun Procedure: Step by Step

Once you've diagnosed the failure and confirmed idempotency, rerun in a controlled way. First, clear the failure condition if one exists (e.g., delete the 'JOB_FAILED' condition). Second, use 'Rerun' from the job's context menu. Third, monitor the rerun's sysout in real time. Fourth, if the job has downstream dependencies, verify they trigger correctly after the rerun. Never rerun a job that's part of a critical path without notifying the on-call team—they may be investigating the same failure.

safe_rerun_checklist.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Pre-rerun checklist for a Control-M job.

JOB_NAME="PAYROLL_TRANSFER"
RUN_DATE=$(date +%Y-%m-%d)

# 1. Check if the job already wrote data (idempotency check)
echo "Checking for existing transfers for $RUN_DATE..."
# In real life: query your database
# SELECT COUNT(*) FROM transfers WHERE transfer_date = '$RUN_DATE';
EXISTING_COUNT=0  # mock value

if [ "$EXISTING_COUNT" -gt 0 ]; then
    echo "WARNING: $EXISTING_COUNT transfers already exist for $RUN_DATE."
    echo "Do NOT rerun without manual review."
    exit 1
fi

# 2. Clear failure condition if present
echo "Clearing failure condition for $JOB_NAME..."
ctmcontb -DELETE "${JOB_NAME}_FAILED" "$(date +%m%d)"

# 3. Rerun the job by its order number
echo "Rerunning $JOB_NAME..."
ctmpsm -UPDATEAJF "$ORDERNO" RERUN

echo "Rerun initiated. Monitor sysout for errors."
Output
Checking for existing transfers for 2025-03-21...
Clearing failure condition for PAYROLL_TRANSFER...
Rerunning PAYROLL_TRANSFER...
Rerun initiated. Monitor sysout for errors.
⚠ Production Trap:
Rerunning a job that has already partly completed can duplicate its side effects. Read the sysout to see exactly where it failed, and make sure the job is safe to repeat before you rerun it.

When the Failure Is in the Script, Not the Scheduler

Sometimes the job ends Not OK because the script itself has a bug—a missing file, a bad variable, or a command that returns non-zero on success (like grep with no matches). These are the easiest to fix but the hardest to spot because the Control-M log only shows the exit code. The fix is to make your scripts exit 0 on success and non-zero only on real failures. Use set -e carefully—it can cause false failures on commands like grep that return 1 for 'no match'. Always test your script outside Control-M first, then wrap it in a job that captures the exit code explicitly.

script_exit_code_fix.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Fix a script that returns non-zero on success due to grep.

LOG_FILE="/tmp/app.log"
echo "ERROR: something happened" > "$LOG_FILE"

# BAD: grep returns 1 if no match, causing false failure
grep -q "CRITICAL" "$LOG_FILE"
# This would exit 1, marking the job Not OK even though no critical error exists.

# GOOD: capture grep result and exit 0 if no match
grep -q "CRITICAL" "$LOG_FILE"
if [ $? -eq 0 ]; then
    echo "Critical error found!"
    exit 1
else
    echo "No critical errors. Success."
    exit 0
fi
Output
No critical errors. Success.
💡Senior Shortcut:
Add || true after commands that return non-zero on benign conditions, or explicitly check $? and exit 0. Never let a trailing grep determine your job's success.

Rerunning with Dependencies: Don't Break the Chain

If the failed job has downstream dependents, rerunning it may trigger them prematurely or leave them waiting. Control-M handles this via conditions and the job's 'Rerun' behavior. When you rerun a job, it will set its out-conditions again, which may trigger downstream jobs that already ran. To avoid duplicate downstream runs, either set the downstream jobs to 'Wait for condition' with a date check, or use Control-M's 'Rerun' option that does not re-trigger dependents. The safest approach: rerun the failed job, then manually verify each downstream job's status and rerun only those that actually need it.

dependency_rerun.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Simulate checking downstream dependencies after a rerun.

FAILED_JOB="SALES_LOAD"
DOWNSTREAM_JOBS=("SALES_TRANSFORM" "SALES_REPORT")

echo "Rerunning $FAILED_JOB..."
# ctm run order <server> <folder> "$FAILED_JOB"

for job in "${DOWNSTREAM_JOBS[@]}"; do
    echo "Checking status of $job..."
    # In real Control-M: ctm run jobs:status::get -s "jobname=$job"
    # Mock: assume SALES_TRANSFORM already ran, SALES_REPORT did not
    if [ "$job" == "SALES_TRANSFORM" ]; then
        echo "  $job already completed. Skipping rerun to avoid duplication."
    else
        echo "  $job did not run. It will trigger automatically from condition."
    fi
done
Output
Rerunning SALES_LOAD...
Checking status of SALES_TRANSFORM...
SALES_TRANSFORM already completed. Skipping rerun to avoid duplication.
Checking status of SALES_REPORT...
SALES_REPORT did not run. It will trigger automatically from condition.
⚠ Never Do This:
Rerunning a job without checking downstream status can cause duplicate reports or double-charges. Always verify which downstream jobs already ran and which are still waiting.

When Not to Rerun: The Manual Intervention Cases

Some failures should never be rerun automatically. If the job failed due to data corruption, a schema change, or a business rule violation, rerunning will just fail again or cause more damage. Examples: a job that loads data into a table with a unique constraint violation, a job that sends customer emails, or a job that calls a third-party API with rate limits. In these cases, you must fix the root cause first—clean the data, update the schema, or wait for the rate limit to reset—then rerun. The rule: if the failure is deterministic (same input, same error), rerunning is pointless. Fix the input or the code first.

deterministic_failure.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# io.thecodeforge — DevOps tutorial
# Detect a deterministic failure that should not be rerun.

ERROR_MSG="ORA-00001: unique constraint (HR.EMP_EMAIL_UK) violated"

echo "Job failed with: $ERROR_MSG"

if echo "$ERROR_MSG" | grep -q "unique constraint"; then
    echo "Deterministic failure detected: duplicate data exists."
    echo "Do NOT rerun. Fix the data first:"
    echo "  DELETE FROM employees WHERE email = 'duplicate@example.com';"
    echo "Then rerun the job."
    exit 1
fi

echo "Transient failure. Safe to rerun."
Output
Job failed with: ORA-00001: unique constraint (HR.EMP_EMAIL_UK) violated
Deterministic failure detected: duplicate data exists.
Do NOT rerun. Fix the data first:
DELETE FROM employees WHERE email = 'duplicate@example.com';
Then rerun the job.
💡Interview Gold:
The difference between a junior and a senior operator: juniors rerun on any failure; seniors check if the failure is deterministic. If the same error will occur again, rerunning is just noise.
● Production incidentPOST-MORTEMseverity: high

The Rerun That Doubled the Payroll

Symptom
A nightly payroll job ended Not OK at 2:15 AM. The on-call operator saw the status, assumed a transient network glitch, and clicked 'Rerun' from the monitoring console.
Assumption
The job had failed before writing any data, so rerunning would be safe.
Root cause
The job had actually completed the bank transfer API call but failed on a subsequent logging step due to a full disk. The rerun executed the transfer again, duplicating 1,200 employee payments. The job was not idempotent—it had no deduplication key and no check for prior execution.
Fix
The team added a pre-run check: query the payroll_transfers table for today's batch ID; if a record exists, exit 0 with a warning. They also changed the job to use a unique idempotency key in the API call and set 'Rerun' to only run after manual confirmation.
Key lesson
  • Never rerun a job until you've read its sysout and confirmed it is idempotent—or you've verified it failed before any side effect.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.4 entries
Symptom · 01
Job status Ended Not OK, Control-M log shows only 'Command failed with exit code 1'
→
Fix
1. Right-click job → View Output (sysout). 2. Search for 'ERROR', 'Exception', or 'failed'. 3. If sysout is empty, check the job's command path and user permissions. 4. Fix the root cause in the script or environment, then rerun.
Symptom · 02
Job Ended Not OK with 'Condition not met' or 'Wait for condition' timeout
→
Fix
1. Open job properties → Conditions tab. 2. Identify the missing condition. 3. Use ctmcontb -LIST <condition> to see if it exists. 4. Find the upstream job that should set it and check its status. 5. Rerun upstream job first, then downstream.
Symptom · 03
Job Ended Not OK after a database deadlock or lock timeout
→
Fix
1. Check DB lock monitor (e.g., SELECT * FROM pg_locks for PostgreSQL). 2. Identify blocking session. 3. Kill the blocker or wait for it to finish. 4. Rerun the job.
Symptom · 04
Job Ended Not OK but the script actually completed its work (false negative)
→
Fix
1. Check the script's exit code handling—often a trailing grep or test returns non-zero. 2. Fix the script to exit 0 on success (e.g., add || true or explicit exit 0). 3. Rerun the job.
★ Control-M Ended Not OK: Find the Real Failure and Rerun Safely Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
`Ended Not OK` with no obvious error in Control-M log−
Immediate action
Open the job's sysout to find the real error.
Commands
In Control-M GUI: right-click job → View Output
grep -i 'error\|exception\|failed' /path/to/sysout.log
Fix now
Fix the error shown in sysout, then rerun.
`Condition not met` or `Wait for condition` timeout+
Immediate action
Check if the condition exists and which job should set it.
Commands
ctmcontb -LIST <condition_name> <date>
ctmpsm -UPDATEAJF <orderno> HOLD
Fix now
Rerun upstream job first, then downstream.
`ORA-00001: unique constraint violated` or similar deterministic error+
Immediate action
Do not rerun. Fix the data first.
Commands
SELECT * FROM table WHERE unique_column = 'value';
DELETE FROM table WHERE unique_column = 'value';
Fix now
Clean the duplicate data, then rerun.
Job failed but you're unsure if it's safe to rerun+
Immediate action
Check if the job is idempotent by looking for existing side effects.
Commands
SELECT COUNT(*) FROM target_table WHERE run_date = CURRENT_DATE;
ls -l /path/to/output/file
Fix now
If side effects exist, do not rerun without manual cleanup. Otherwise, rerun.
AspectRerun (Safe)Rerun from Scratch (Risky)
Use caseJob failed after partial work, idempotentJob failed before any side effect
Data duplication riskLow if idempotentHigh if not idempotent
Dependency handlingMay re-trigger downstreamRe-triggers all downstream
Recommended forMost transient failuresOnly when job is proven safe to repeat
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
inspect_job_failure.shJOB_LOG="/tmp/controlm_job_$$.log"Why 'Ended Not OK' Is a Symptom, Not a Diagnosis
sysout_vs_ctlmlog.shcat > /tmp/db_job.sh <<'EOF'Reading the Sysout
condition_trace.shecho "Condition: DAILY_SALES_LOADED"Conditions
idempotent_payment.pyfrom datetime import dateIdempotency
safe_rerun_checklist.shJOB_NAME="PAYROLL_TRANSFER"The Safe Rerun Procedure
script_exit_code_fix.shLOG_FILE="/tmp/app.log"When the Failure Is in the Script, Not the Scheduler
dependency_rerun.shFAILED_JOB="SALES_LOAD"Rerunning with Dependencies
deterministic_failure.shERROR_MSG="ORA-00001: unique constraint (HR.EMP_EMAIL_UK) violated"When Not to Rerun

Key takeaways

1
Ended Not OK is a symptom, not a diagnosis—the real error is always in the sysout or a missing condition.
2
Never rerun a job until you've confirmed it's idempotent or verified it failed before any side effect.
3
Deterministic failures (unique constraint violations, schema errors) will fail again on rerun—fix the root cause first.
4
The safest rerun pattern is to build idempotency into every job from day one using unique keys and existence checks.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How does Control-M determine that a job ended Not OK, and where does the...
Q02SENIOR
When would you choose to rerun a job versus rerun from scratch, and what...
Q03SENIOR
A job fails with a unique constraint violation. What's your mitigation, ...
Q04SENIOR
How do you safely rerun a job that has downstream dependencies without t...
Q05SENIOR
What's the difference between a transient failure and a deterministic fa...
Q06SENIOR
How would you design a Control-M job to be idempotent so it can be safel...
Q01 of 06JUNIOR

How does Control-M determine that a job ended Not OK, and where does the real failure information live?

ANSWER
Control-M sets Ended Not OK based on the job's exit code (non-zero), a failed post-processing condition, or an On/Do output rule that sets NOTOK. The real failure is in the job's sysout (application output) or the Control-M log (scheduler events). The status is just a signal; you must read the sysout to find the root cause.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
How do I find the real error when a Control-M job ends Not OK?
02
What's the difference between Rerun and Set to OK in Control-M?
03
How do I safely rerun a Control-M job that writes to a database?
04
Why does my Control-M job end Not OK even though the script succeeded?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
October 31, 2026
last updated
2,099
articles · all by Naren
🔥

That's Control-M. Mark it forged?

3 min read · try the examples if you haven't

←
Previous
Control-M Job Stuck in Wait Condition: Condition Dates and ODAT Explained
3 / 4 · Control-M
Next
Control-M Interview Questions: 40 Real Production Q&As
→