---
title: FB vs MAIN Action — TwinCAT PLC architecture decision
title_en: FB vs MAIN Action — TwinCAT PLC architecture decision
description: A decision record breaking down Function Block vs MAIN Action on TwinCAT 3 — across essence, instance fan-out, debug observability, maintenance cost, and community practice — with a verdict for an 11-axis Tray Inverter machine.
sidebar_label: FB vs MAIN Action
---

import styles from './fb-vs-action-analysis.module.css';

export const fbAxes = ['RotMaster', 'RotL', 'RotR', 'LiftZ', 'GripL', 'GripR', 'ConvLd', 'ConvUld', 'BarLD_Z', 'BarULD_Z', 'PusherX'];
export const fbStates = ['DONE', 'BUSY', 'IDLE', 'DONE', 'BUSY', 'IDLE', 'DONE', 'IDLE', 'BUSY', 'IDLE', 'DONE'];
export const actionAxes = ['AX0', 'AX1', 'AX2', 'AX3', 'AX4', 'AX5', 'AX6', 'AX7', 'AX8', 'AX9', 'AX10'];

<div className={styles.page}>
<div className={styles.main}>

<section className={styles.hero}>
  <span className={styles.heroTag}>PLC Architecture Decision Record</span>
  <h1>
    <span className={styles.fbToken}>Function Block</span><br />
    vs.<br />
    <span className={styles.actionToken}>MAIN Action</span>
  </h1>
  <p className={styles.lead}>
    Code organization choice for a Tray Inverter machine on TwinCAT 3. This article breaks down how the two styles differ in maintenance cost, debug observability, and community practice, then lands on a verdict for this specific machine.
  </p>
  <div className={styles.heroMeta}>
    <span><strong>Machine</strong>Tray Inverter</span>
    <span><strong>Controller</strong>Beckhoff TwinCAT 3</span>
    <span><strong>Date</strong>2026-05-12</span>
  </div>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 1 &nbsp; ESSENCE</div>
  <h2 className={styles.secTitle}>Essential difference</h2>
  <p className={styles.secDesc}>Under IEC 61131-3 these are different levels of abstraction. A Function Block is a stateful, instantiable unit; an ACTION is just a named sub-snippet inside a program.</p>

  <div className={styles.compareGrid}>
    <div className={`${styles.compareCard} ${styles.fb}`}>
      <h3>Function Block</h3>
      <div className={styles.compareCardSub}>FUNCTION_BLOCK FB_TaskMoveAbs</div>
      <dl>
        <div><dt>State</dt><dd>Each instance owns its own VAR (FSM, MC_* instance)</dd></div>
        <div><dt>Parameters</dt><dd>Explicit signature via VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT</dd></div>
        <div><dt>Reuse</dt><dd>The same FB instantiates N times, each running independently</dd></div>
        <div><dt>OOP</dt><dd>METHOD / PROPERTY / INTERFACE / EXTENDS</dd></div>
        <div><dt>Online change</dt><dd>Impact scoped to the FB</dd></div>
        <div><dt>Cross-reference</dt><dd>Strong type / input naming makes Find References reliable</dd></div>
      </dl>
    </div>

    <div className={`${styles.compareCard} ${styles.action}`}>
      <h3>MAIN Action</h3>
      <div className={styles.compareCardSub}>ACTION MoveAbs : BOOL</div>
      <dl>
        <div><dt>State</dt><dd>Shares MAIN's VAR — a single global copy</dd></div>
        <div><dt>Parameters</dt><dd>None; values flow through MAIN's global variables</dd></div>
        <div><dt>Reuse</dt><dd>Not possible — only copy-paste or a giant CASE</dd></div>
        <div><dt>OOP</dt><dd>None</dd></div>
        <div><dt>Online change</dt><dd>Treated as a change to the entire MAIN program</dd></div>
        <div><dt>Cross-reference</dt><dd>Can only search variable names — high noise</dd></div>
      </dl>
    </div>
  </div>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 2 &nbsp; INSTANCE FAN-OUT</div>
  <h2 className={styles.secTitle}>11 servos, 11 separate states</h2>
  <p className={styles.secDesc}>Each of the 11 servos on this machine needs its own MC_MoveAbsolute instance, its own eState, and its own xBusy / xDone. The diagram shows how an FB fans out into 11 independent instances, while ACTION forces every axis to share one state space.</p>

  <div className={styles.fanoutStage}>
    <div className={`${styles.fanoutPanel} ${styles.fb}`}>
      <h4>FB instances</h4>
      <div className={styles.ph}>FB_TaskMoveAbs &times; 11</div>
      <svg className={styles.svgStage} viewBox="50 20 380 340" xmlns="http://www.w3.org/2000/svg">
        <rect className={`${styles.coreBlock} ${styles.fb}`} x="180" y="160" width="120" height="60" rx="4" />
        <text className={styles.coreLabel} x="240" y="185">FB_TaskMoveAbs</text>
        <text className={`${styles.coreLabel} ${styles.dim}`} x="240" y="202">FUNCTION_BLOCK</text>
        {fbAxes.map((name, i) => {
          const cx = 240, cy = 190, r = 145;
          const angle = (i / fbAxes.length) * Math.PI * 2 - Math.PI / 2;
          const x = cx + Math.cos(angle) * r;
          const y = cy + Math.sin(angle) * r;
          const delay = 0.1 + i * 0.08;
          return (
            <g key={name}>
              <line x1={cx} y1={cy} x2={x} y2={y} className={styles.connFb} style={{animationDelay: `${delay}s`}} />
              <rect x={x - 38} y={y - 14} width="76" height="28" rx="3" className={styles.instBubble} style={{animationDelay: `${delay + 0.1}s`}} />
              <text x={x} y={y - 2} className={styles.instLabel} style={{animationDelay: `${delay + 0.2}s`}}>[{i}] {name}</text>
              <text x={x} y={y + 9} className={styles.instState} style={{animationDelay: `${delay + 0.25}s`}}>{fbStates[i]}</text>
            </g>
          );
        })}
      </svg>
    </div>

    <div className={`${styles.fanoutPanel} ${styles.action}`}>
      <h4>MAIN Action</h4>
      <div className={styles.ph}>11 axes sharing MAIN.VAR</div>
      <svg className={styles.svgStage} viewBox="50 20 380 340" xmlns="http://www.w3.org/2000/svg">
        <rect className={`${styles.sharedBlob} ${styles.pulseAction}`} x="170" y="150" width="140" height="80" rx="4" />
        <text className={styles.sharedState} x="240" y="180">MAIN.eState</text>
        <text className={styles.sharedState} x="240" y="198" style={{fontSize: '11px'}}>MAIN.nAxisId</text>
        <text className={styles.sharedWarn} x="240" y="218">single state, 11 callers</text>
        {actionAxes.map((name, i) => {
          const cx = 240, cy = 190, r = 150;
          const angle = (i / actionAxes.length) * Math.PI * 2 - Math.PI / 2;
          const x = cx + Math.cos(angle) * r;
          const y = cy + Math.sin(angle) * r;
          const bx = cx + Math.cos(angle) * 75;
          const by = cy + Math.sin(angle) * 50;
          const delay = 0.1 + i * 0.06;
          return (
            <g key={name}>
              <line x1={x} y1={y} x2={bx} y2={by} className={styles.connAction} style={{animationDelay: `${delay}s`}} />
              <circle cx={x} cy={y} r="14" className={styles.axisNode} />
              <text x={x} y={y + 3} className={styles.axisLabel}>{name}</text>
            </g>
          );
        })}
      </svg>
    </div>
  </div>

  <p className={styles.fanoutFootnote}>
    In practice, the ACTION route has only two variants:
    {' '}<strong>(a)</strong> hand-copy 11 versions — change one place, change 11 places;
    {' '}<strong>(b)</strong> declare 11 axis-specific global variables plus a giant CASE — hand-rolling OOP in a procedural style and killing Cross-reference.
  </p>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 3 &nbsp; SCOPE OF THIS MACHINE</div>
  <h2 className={styles.secTitle}>11 local axes + Master/Slave coupling</h2>
  <p className={styles.secDesc}>Below is the servo list for this machine. AX_Flip_RotMaster fans out to RotL / RotR through the NC <code className="inline">MC_GearIn</code> coupling — exactly where instance-level state management proves its worth.</p>
  <div className={styles.axisRail}>
    <div className={`${styles.axisChip} ${styles.axisChipMaster}`}>RotMaster<div className={styles.role}>virtual</div></div>
    <div className={styles.axisChip}>RotL<div className={styles.role}>slave</div></div>
    <div className={styles.axisChip}>RotR<div className={styles.role}>slave</div></div>
    <div className={styles.axisChip}>LiftZ<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>GripL<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>GripR<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>ConvLd<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>ConvUld<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>BarLD_Z<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>BarULD_Z<div className={styles.role}>servo</div></div>
    <div className={styles.axisChip}>PusherX<div className={styles.role}>servo</div></div>
  </div>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 4 &nbsp; DEBUG OBSERVABILITY</div>
  <h2 className={styles.secTitle}>Online view: visible vs invisible</h2>
  <p className={styles.secDesc}>Whether a running PLC is actually debuggable comes down to whether you can see every axis's state at the same moment. The mockup below shows TwinCAT online view under each architecture.</p>

  <div className={styles.debugGrid}>
    <div className={`${styles.debugPanel} ${styles.fb}`}>
      <h4>FB — per-instance watch</h4>
      <div className={styles.watchWindow}>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[0].eState</span><span className={`${styles.watchVal} ${styles.pulseFb}`}>DONE</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[1].eState</span><span className={styles.watchValBusy}>BUSY</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[2].eState</span><span className={styles.watchVal}>DONE</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[3].eState</span><span className={styles.watchValErr}>ERROR</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[4].eState</span><span className={styles.watchValBusy}>BUSY</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[5].eState</span><span className={styles.watchVal}>IDLE</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[6].eState</span><span className={styles.watchVal}>IDLE</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[7].eState</span><span className={styles.watchValBusy}>BUSY</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[8].eState</span><span className={styles.watchVal}>DONE</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[9].eState</span><span className={styles.watchVal}>IDLE</span></div>
        <div className={styles.watchRow}><span className={styles.watchVar}>fbMoveAbs[10].eState</span><span className={styles.watchVal}>IDLE</span></div>
      </div>
      <p className={styles.panelNote}>All 11 parallel tasks' real states are visible and comparable at the same instant. A breakpoint can pin to "the instance for axis 3" and hit precisely.</p>
    </div>

    <div className={`${styles.debugPanel} ${styles.action}`}>
      <h4>ACTION — shared state</h4>
      <div className={styles.actionBlackbox}>
        <div className={styles.actionBlackboxBig}>MAIN.eState = <span className={styles.qmark}>???</span></div>
        <div>Who wrote it? Which axis is moving?</div>
        <div className={styles.blackboxComment}>
          {'// axis index lives in MAIN.nAxisId'}<br />
          {'// but nAxisId got overwritten the previous cycle'}
        </div>
        <div className={styles.blackboxAlert}>Cannot distinguish state across 11 axes</div>
      </div>
      <p className={styles.panelNote}>All variables share one copy in MAIN. A breakpoint can only attach to a code line; when 11 axes run through the same line, you can't tell them apart. Synchronization / fan-out scenarios become nearly undebuggable.</p>
    </div>
  </div>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 5 &nbsp; MAINTENANCE COST</div>
  <h2 className={styles.secTitle}>Change one place vs change 11 places</h2>
  <p className={styles.secDesc}>Maintenance cost isn't about how pretty today's code is — it's about how many places need to change when requirements shift three months out.</p>

  <div className={styles.prosCons}>
    <div className={`${styles.pcCard} ${styles.fb}`}>
      <h4>Function Block</h4>
      <ul>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>Change one FB and all 11 axes pick it up; regression scope is clear</span></li>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>VAR_INPUT / VAR_OUTPUT acts as an interface contract — team handoffs and C# bridges read directly from the signature</span></li>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>Online change impact stays narrow, keeping hot-patch risk controllable</span></li>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>Unit tests can isolate each FB individually</span></li>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>Find All References has a high hit rate</span></li>
        <li><span className={`${styles.badge} ${styles.badgeNeu}`}>NEU</span><span>First-time instance declaration costs an extra line</span></li>
      </ul>
    </div>

    <div className={`${styles.pcCard} ${styles.action}`}>
      <h4>MAIN Action</h4>
      <ul>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>Useful for splitting a short linear sequence visually; up to ~5 lines it can improve readability</span></li>
        <li><span className={`${styles.badge} ${styles.badgePro}`}>PRO</span><span>No instance to declare — saves a line on first write</span></li>
        <li><span className={`${styles.badge} ${styles.badgeCon}`}>CON</span><span>MAIN's variable space balloons into a "god program" as the machine grows</span></li>
        <li><span className={`${styles.badge} ${styles.badgeCon}`}>CON</span><span>Cannot fan out — multi-axis scenarios force copy-paste</span></li>
        <li><span className={`${styles.badge} ${styles.badgeCon}`}>CON</span><span>No input / output signature; the C# integration layer struggles to align</span></li>
        <li><span className={`${styles.badge} ${styles.badgeCon}`}>CON</span><span>Online change scope equals the entire MAIN — risk amplified</span></li>
      </ul>
    </div>
  </div>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 6 &nbsp; INDUSTRY CONSENSUS</div>
  <h2 className={styles.secTitle}>Where the community lands</h2>
  <p className={styles.secDesc}>This one is lopsided. The chart below shows where major Beckhoff / IEC 61131-3 communities sit on "which to use at the architecture level". ACTION retains residual value only for "splitting a short sub-snippet inside an FB".</p>

  <div className={styles.consensusStage}>
    <div className={styles.sourceRow}>
      <span className={styles.sourceName}>Beckhoff InfoSys (official)</span>
      <div className={styles.sourceBar}><div className={styles.fill} style={{width: '96%'}}></div></div>
      <span className={styles.sourcePct}>FB 96%</span>
    </div>
    <div className={styles.sourceRow}>
      <span className={styles.sourceName}>PLCopen / IEC 61131-3</span>
      <div className={styles.sourceBar}><div className={styles.fill} style={{width: '94%', animationDelay: '0.1s'}}></div></div>
      <span className={styles.sourcePct}>FB 94%</span>
    </div>
    <div className={styles.sourceRow}>
      <span className={styles.sourceName}>Beckhoff Forum (large projects)</span>
      <div className={styles.sourceBar}><div className={styles.fill} style={{width: '92%', animationDelay: '0.2s'}}></div></div>
      <span className={styles.sourcePct}>FB 92%</span>
    </div>
    <div className={styles.sourceRow}>
      <span className={styles.sourceName}>plctalk.net</span>
      <div className={styles.sourceBar}><div className={styles.fill} style={{width: '85%', animationDelay: '0.3s'}}></div></div>
      <span className={styles.sourcePct}>FB 85%</span>
    </div>
    <div className={styles.sourceRow}>
      <span className={styles.sourceName}>reddit r/PLC</span>
      <div className={styles.sourceBar}><div className={styles.fill} style={{width: '88%', animationDelay: '0.4s'}}></div></div>
      <span className={styles.sourcePct}>FB 88%</span>
    </div>
    <div className={styles.sourceRow}>
      <span className={styles.sourceName}>STEP7 / Codesys V2 (legacy)</span>
      <div className={styles.sourceBar}><div className={styles.fillAction} style={{width: '55%', animationDelay: '0.5s'}}></div></div>
      <span className={styles.sourcePct}>ACTION 55%</span>
    </div>
    <p className={styles.consensusNote}>
      ※ Percentages are qualitative estimates reflecting the mainstream recommendation ratio across these communities for "FB vs ACTION at the architecture level" — not rigorous survey data.
    </p>
  </div>
</section>

<section className={styles.block}>
  <div className={styles.secLabel}>§ 7 &nbsp; VERDICT</div>
  <h2 className={styles.secTitle}>Verdict for this machine</h2>

  <div className={styles.verdict}>
    <h3>Keep the current FB decomposition.</h3>
    <p>
      This machine's profile — 11 axes running in parallel plus Master/Slave coupling — makes per-instance observability a hard requirement for debugging. Building the architecture on ACTION would mean giving up instance-level state inspection and bounded Online change scope.
    </p>
    <p>
      ACTION's reasonable role downgrades to <strong>a layout-splitting tool inside an FB</strong> — for example a 5-line "reset latch" sub-snippet inside <code>MAIN_Alarm</code>, or a named partition for each FSM state inside an FB.
    </p>
    <p>
      Architecture-level concerns (task dispatch, interlock, motion task) stay in FB. The current <code>FB_PlcTaskDispatcher</code> / <code>FB_TaskMoveAbs</code> / <code>FB_TaskHome</code> / <code>FB_Interlock</code> decomposition is correctly oriented and does not need refactoring.
    </p>
  </div>
</section>

<footer className={styles.footer}>
  Tray Inverter / Beckhoff TwinCAT 3 &nbsp; · &nbsp; ASE_TrayInverterSystem &nbsp; · &nbsp; 2026-05-12
</footer>

</div>
</div>
