How to Write Sub Program in CNC Machine
A subprogram is one block of G-code that the control runs again and again on command. This guide shows the M98 and G65 syntax, the parameter passing rules, and the mistakes that stop a cycle mid-cut. Written for programmers and setup machinists who need code that runs the first time.

In this article
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
Key takeaways
What a subprogram is and when it pays off
A subprogram is a separate program stored under its own O number that the main program calls to run. When the control reaches M98 P1001, it jumps to O1001, executes those blocks, and returns at M99 to the line right after the call. The main program keeps its flow. The subprogram holds the repeated work.
The payoff shows up on parts with repeated geometry: a plate with 24 identical M8 tapped holes, a fixture with 8 pockets, a family of brackets that share one bolt pattern. You write the hole cycle once and call it 24 times with different X and Y. Change the feed once, and all 24 holes change.
Do not reach for a subprogram just because code looks long. If a sequence runs only twice, plain copy-paste is easier to read and easier to restart. Subprograms earn their place when the same motion repeats three or more times, or when several programs share one canned sequence.
Keep the subprogram free of machine-specific assumptions where you can. If O1001 contains a G54 work offset, every program that calls it inherits that offset. That is fine on one machine and a crash on another. Put the offset in the main program and let the subprogram handle only geometry.
- 1Good fitRepeated hole patterns, shared pocket cycles, family-of-parts bolt circles.
- 2Poor fitA sequence used once or twice with no parameter variation.
- 3Watch the offsetKeep G54–G59 in the main program, not inside the subprogram.
M98 and M99: the call and the return
M98 is the call. Its P word gives the subprogram number. In M98 P1001, the control reads 1001 as the program number and runs O1001. Some builders also accept an L word for repeat count, for example M98 P1001 L6, which runs the subprogram six times in a row without writing six call blocks.
M99 is the return. Put it as the last block of the subprogram. The control then jumps back to the block after the M98 call. If the subprogram ends with M30 instead, the control rewinds the whole program and stops. On a mid-cycle tap that means a broken tap or a scrapped part.
There is one more use for M99 that surprises people. If you place M99 at the end of the main program instead of M30, the control loops the whole program and runs it again. That is handy for a bar feeder cycle, and dangerous if you forgot it is there.
Call depth matters. A subprogram may call another subprogram. Most Fanuc-style controls allow 4 levels. Push past that and you get a P/S alarm and the machine stops. If your logic needs 5 levels, flatten it or split the work into two main programs.
- 1CallM98 P1001 — jump to O1001 and return at M99.
- 2RepeatM98 P1001 L6 — run the same subprogram six times.
- 3ReturnM99 — back to the block after the call.
- 4Never end a subprogram with M30It rewinds and stops the cycle.
Passing values with G65 and local variables
Plain M98 carries no data. Every call runs the identical motion. If you want one subprogram to cut 24 holes at 24 positions, you either write 24 call lines with the position in the main program, or you use G65 and pass the position as an argument.
G65 P9010 X50. Y30. Z-12. R2. F180 calls O9010 and loads the values into local variables. X maps to #24, Y to #25, Z to #26, R to #18, F to #9. Inside the subprogram you write G81 Z#26 R#18 F#9 and the control substitutes the numbers you passed.
Local variables #1 through #33 are cleared when the subprogram returns on most controls. That is a feature. It means the next call starts clean, with no leftover value from the previous position. If you need a value to survive the return, write it to a common variable such as #100 or #500.
Arguments are not free. Each G65 call costs a small amount of cycle time versus a plain M98, and deeply nested G65 calls get hard to trace when a dimension is wrong. For a fixed pattern that never changes, plain M98 is simpler. For a pattern driven by a table of positions, G65 is the cleaner route.
- 1X → #24, Y → #25, Z → #26Standard argument mapping on Fanuc-style controls.
- 2R → #18, F → #9Useful for canned cycles inside the subprogram.
- 3Local variables reset on returnUse #100+ if a value must persist.
Naming, numbering, and what belongs inside
Give the subprogram its own O number in a range your shop reserves for subroutines. A common habit is O1000–O1999 for shared subprograms and O9000–O9999 for machine-builder or macro programs. Keep a written log. Two programmers assigning O1050 to different code is a real crash waiting to happen.
The first blocks should set a known state. Put a safe Z, a G80 to cancel any canned cycle, and the modal values the subprogram needs. Never assume the main program left the control in a convenient mode. A subprogram that starts with G91 by accident will drill holes in a line instead of a circle.
Write the subprogram so it can run from any starting point. Use absolute positioning for pattern geometry unless incremental motion is genuinely what you want. Incremental code that works when called from one spot often fails when called from another, because the machine is somewhere unexpected.
End with the machine in a predictable place. Lift to a clearance plane, cancel the cutter comp with G40, cancel the cycle with G80, then M99. The next block in the main program should not have to clean up after the subprogram.
- 1Reserve a number rangeO1000–O1999 for shared subroutines, logged in one place.
- 2Set state on entryG80, G40, safe Z, and the modal values you rely on.
- 3Leave state clean on exitG40, G80, clearance Z, then M99.
Step by step: write, test, and call the subprogram
Follow the order. Skipping step 5 is how most first-run crashes happen.
- 1Define the job of the subprogramWrite one sentence: 'drill one M8 hole at the position passed in X and Y.' If the sentence contains 'and', the subprogram is doing too much. Split it. A subroutine that drills and taps is two subroutines.
- 2Assign a program number and a commentPick a free number from your reserved range, for example O1050, and add a comment on the same line: O1050 (M8 DRILL CYCLE). The comment is what the next machinist reads at 2 a.m.
- 3Set the entry stateStart with G80 to cancel any live canned cycle, G40 to cancel cutter comp, and a G0 Z50. to reach a known clearance. Then set the modal values the cycle needs, such as G90 absolute and G17 XY plane.
- 4Write the motion blocksFor a drill cycle: G0 X#24 Y#25, then G81 Z-12. R2. F180. For a pocket: place the cutter, ramp in, run the contour, then G40 and retract. Keep every coordinate either absolute or clearly incremental, never mixed without a comment.
- 5Close the subprogram cleanlyCancel the cycle with G80, cancel comp with G40, lift to the clearance plane with G0 Z50., then write M99. Do not write M30. Do not leave the spindle running unless the next call needs it.
- 6Test in single block with no partLoad the main program, set rapid override to 25%, and step through the first two calls. Watch the distance-to-go display. Confirm the machine returns to the exact block after each M98 before you let it run at full speed.
- 7Call it from the main programWith G65: G65 P1050 X50. Y30. For a fixed pattern with no arguments: M98 P1050. For a repeat count: M98 P1050 L6. Keep all work offsets (G54–G59) in the main program, not inside the subprogram.
- 8Verify the first part before the runCut one part, check the hole positions against the drawing, and confirm the return point did not drift. On a 24-hole plate, one wrong local variable shows up as a whole row of holes in the wrong place.
M98 vs G65 vs macro: which call to use
Match the call to whether the subprogram needs data handed to it.
| Method | Passes values | Best for | Watch out for |
|---|---|---|---|
| M98 P1001 | No | Fixed sequence, same motion every time | No arguments; position must live in the main program |
| M98 P1001 L6 | No | Same cycle repeated a set number of times | Repeat count must match the actual pattern |
| G65 P9010 X.. Y.. | Yes, into local variables | Position-driven patterns and family of parts | Slightly slower per call; harder to trace a bad dimension |
| G66 modal macro call | Yes | Same macro applied to many following blocks | Easy to leave active by accident; cancel with G67 |
| Plain copy-paste | No | A sequence used once or twice | Editing one copy and forgetting the others |
Common subprogram errors and fixes
Symptom first, then the cause and the fix.
| Symptom | Likely cause | Fix |
|---|---|---|
| Alarm on the M98 line | Program number not in memory or wrong O number | Load O1001 and check the P word matches |
| Cycle stops mid-cut after the last hole | Subprogram ends with M30 instead of M99 | Replace M30 with M99 |
| Machine keeps running the program again | M99 written at the end of the main program | Change the main program end to M30 |
| Second call cuts at the wrong position | Incremental motion left active from the first call | Set G90 at the top of the subprogram |
| Holes drift further off with each call | Comp or cycle not cancelled before return | Add G40 and G80 before M99 |
| P/S alarm during deep nesting | More than 4 levels of subprogram calls | Flatten the logic or split into two main programs |
Write the subprogram once, prove it twice
If the same motion repeats three or more times, a subprogram pays for itself. Test the first two calls in single block before you let the cycle run. When the code is right, send us the drawing and we will quote the parts.
Subprogram questions engineers ask
What is the difference between a subprogram and a macro?
A subprogram is a stored program called with M98. It runs the same blocks every time. A macro is also a stored program, but it can do arithmetic, read system variables, branch with IF and GOTO, and receive arguments through G65.
In practice the line is blurry. Many controls let a subprogram read local variables, so a simple macro is just a subprogram that happens to use #24 and #25. If your code needs math or conditional logic, call it a macro and store it in the O9000 range.
How do I debug a subprogram that cuts the wrong position?
First check whether the subprogram is in absolute or incremental mode. Add a G90 or G91 line at the top and retest. Most position errors come from a leftover G91, not from a wrong coordinate.
If the mode is correct, look at the variable values. On a G65 call, display #24 and #25 in the macro variable screen before the call runs. If the numbers are right there, the error is inside the subprogram. If they are wrong, the error is in the call block.
Can subprograms be nested?
Yes. A subprogram can call another subprogram. Most Fanuc-style controls allow 4 levels of nesting. The main program counts as the first level on some controls and not on others, so test on your machine before you rely on the limit.
If you hit a P/S alarm during nesting, the count is the usual suspect. Flatten the structure by moving the innermost sequence into the caller, or split the job across two main programs.
Are there limitations to using subprograms?
Memory is one. Every subprogram occupies control memory, and older controls have little of it. Cycle time is another. A G65 call is slightly slower than inline code because of argument handling.
The bigger limit is readability. A subprogram that does six different things behind one number is harder to debug than six short programs. Keep each subprogram to one job and log the number range in a place the whole shop can see.
Should I put the work offset inside the subprogram?
No. Keep G54 through G59 in the main program. If the offset lives inside the subprogram, every program that calls it inherits that offset. That is fine on one machine and a crash on another.
The same rule applies to tool length offsets. Call the tool and the offset in the main program, then let the subprogram handle geometry only.
What if the subprogram needs a value to survive the return?
Use a common variable. Local variables #1 through #33 are cleared when the subprogram returns on most controls. Write the value to #100 or #500 before M99 and read it in the main program.
Common variables persist until the control is powered off or you clear them. That is useful for a counter across 24 calls, and dangerous if you forget to reset the counter at the top of the next part.
Turn your G-code into finished parts
Upload a drawing and get a quotation with free DFM analysis within 12 hours. From one prototype to 10,000+ part runs, with 100% inspection before shipment.
12-hour quote100% inspectionNo minimum order quantityNDA on request