The .rungs File Format

Write a .rungs file by hand or with an AI tool. A complete reference to the text file that holds an Add-On Instruction's tags, logic and tests in Rungs Studio.

A .rungs file is the text file Rungs Studio saves your work in. Because it is plain text, you do not need Studio to make one: you can write it in any editor, generate it from a script, or ask an AI tool to write it. Then click Open AOI (.rungs or .L5X) in Studio and run it.

If you use an AI tool, give it this page. Every docs page is also available as plain Markdown at its address plus .md, which is easier for a tool to read.

A complete file

The file is written in YAML, a format that uses indentation to show what belongs to what. This one describes an Add-On Instruction (AOI), a reusable block of PLC logic. The lamp comes on once the button has been held for 2 seconds and goes off when it is released, and the file includes a test that proves it.

delayed-lamp.rungs
version: 6
aoi:
  name: DelayedLamp
  description: Turns the lamp on once the button has been held for 2 seconds.
  parameters:
    - name: In_Button
      description: Push button
      usage: input
      dataType: BOOL
    - name: Out_Lamp
      description: Lamp output
      usage: output
      dataType: BOOL
  localTags:
    - name: Tmr_Delay
      dataType: TIMER
      defaultValue:
        PRE: 2000
  routines:
    Logic:
      type: ld
      content: |-
        XIC(In_Button) TON(Tmr_Delay,?,?);
        XIC(Tmr_Delay.DN) OTE(Out_Lamp);
tests: |
  - name: lamp waits 2 seconds
    steps:
      - in:
          In_Button: 1
        advance:
          time: 1800ms
        expect:
          Out_Lamp: 0
      - advance:
          time: 400ms
        expect:
          Out_Lamp: 1

Top-level fields

FieldRequiredWhat it holds
versionyesThe format version. Use 6. Studio opens this and every older version
aoiyesThe instruction: its name, tags and routines
testsnoThe test suite, as a block of text
ionoStart values for InOut tags

Files Studio saves also have an origin line. It records which part of Studio wrote the file, so leave it out when you write one yourself.

The aoi section

FieldRequiredWhat it holds
nameyesThe instruction's name. It does not have to match the file's name
descriptionnoOne line that says what it does
parametersnoThe tags the instruction shares with its caller, in order
localTagsnoThe tags only the instruction can see
routinesyesThe logic

A name can be up to 40 characters. It must start with a letter or underscore and can contain only letters, digits and underscores. If the name is already an instruction or a data type, Studio adds _AOI to it, so TIMER opens as TIMER_AOI.

Tags

A tag is a named value your logic reads or writes. Each entry in parameters and localTags has these fields:

FieldUsed onWhat it holds
nameevery tagLetters, digits and underscores, starting with a letter or underscore
dataTypeevery tagOne of the types below
usageparameters onlyinput, output or inout
descriptionany tagWhat the tag is for
defaultValuenot InOutThe value the tag starts with. Leave it out to start at 0
dimensionLocal and InOutMakes the tag an array of this many elements. Use one number only

Arrays count from 0, so a dimension of 3 gives [0], [1] and [2], and the index can be a tag, as in Ref_Values[Wrk_Index]. Tag names must be unique, and Speed and speed count as the same name. Words the language keeps for itself, such as IF or TRUE, cannot be tag names.

Which data types you can use depends on the kind of tag. Tag usage explains why, and data types describes each one.

KindData types
Input and OutputBOOL, SINT, INT, DINT, REAL
Local and InOutThe types above, plus TIMER, COUNTER, CONTROL, FBD_TIMER, FBD_COUNTER, FBD_ONESHOT and STRING

Write BOOL values as 0 and 1. Give a timer or counter its preset under defaultValue, as Tmr_Delay does above, and a STRING its text. In a TIMER or FBD_TIMER, PRE and ACC are in milliseconds, so 2000 is 2 seconds.

To start one element of a Local array at a value, use elements with the element's number in quotes:

- name: Wrk_Buf
  dataType: DINT
  dimension: 3
  elements:
    '1':
      defaultValue: 5

You do not need to add EnableIn and EnableOut. Every AOI has them, and Studio adds them when it opens the file.

Routines

A routine is a block of logic. Logic is the one that runs on every scan. Prescan and EnableInFalse are optional, and most files do not need them.

Each routine has a type, either ld for Ladder Diagram or st for Structured Text, and its code in content. Write content: |- and indent the code beneath it, so that YAML keeps your line breaks.

Ladder Diagram

Ladder is written as text, with one rung per line and a ; at the end of each rung. Spaces between instructions make no difference. Rungs run from top to bottom on every scan, so a rung sees what the rungs above it wrote on the same scan. Any tag can be read, including an Output, which is how a seal-in works.

  • In series (AND): put instructions next to each other. XIC(In_A) XIC(In_B) OTE(Out_Lamp);
  • In parallel (OR): put the paths in square brackets, separated by commas. [XIC(In_A), XIC(In_B)] OTE(Out_Lamp);. A path can hold several instructions, as in [XIC(In_A) XIO(In_B), XIC(In_C)], and branches can sit inside branches.
  • Timers and counters: write ? in the last two places, and set the preset on the tag. TON(Tmr_Delay,?,?);
  • Reading a member: use a dot. XIC(Tmr_Delay.DN) reads the timer's done bit.
  • Fixed numbers: an operand can be a number. EQ(Wrk_Step, 2) and MOVE(0, Wrk_Step)

These are the instructions most programs need. The result always goes last. The ladder instruction pages list the rest.

InstructionWhat it does
XIC(tag)Passes power when the tag is 1
XIO(tag)Passes power when the tag is 0
OTE(tag)Sets the tag to 1 while the rung is true, otherwise to 0
OTL(tag), OTU(tag)Latches the tag to 1, or unlatches it to 0
ONS(tag)Passes power for one scan. Give it its own BOOL tag
TON(timer,?,?), TOF(timer,?,?), RTO(timer,?,?)On-delay, off-delay and retentive timers
CTU(counter,?,?), CTD(counter,?,?)Counts up or down each time the rung turns true
RES(tag)Clears a timer or counter
MOVE(source,destination)Copies a value
ADD(a,b,result), SUB, MUL, DIVArithmetic
EQ(a,b), NE, GT, GE, LT, LEPasses power when the comparison is true

Every timer and counter has PRE, the preset, and ACC, the amount so far. You can use a member anywhere you can use a tag, as in MOVE(Cnt_Parts.ACC,Out_Count). A rung with no conditions, such as that MOVE on its own, runs on every scan.

MemberMeaning
TIMER: DNTON, RTO: on when ACC reaches PRE. TOF: on while the rung is true and until the delay ends
TIMER: EN, TTOn while the rung is true, and on while the timer is timing
COUNTER: DNOn while ACC is at or above PRE. Counting carries on past PRE
COUNTER: CU, CD, OV, UNThe rung state of CTU and CTD, and overflow and underflow

A TON clears ACC and DN when its rung goes false. An RTO keeps its time until RES. RES clears ACC and the status bits, including CU, so a count input still on when a reset ends counts once more.

Anything that reacts to a rung going from false to true, such as a counter or ONS, needs to see the rung false first. A rung already true on the very first scan does nothing.

Studio uses the current instruction names, and the older ones do not work:

Older nameUse
EQU, NEQEQ, NE
GRT, GEQ, LES, LEQGT, GE, LT, LE
MOVMOVE
LIM, SQR, TRN, XPYLIMIT, SQRT, TRUNC, EXPT
ACS, ASN, ATNACOS, ASIN, ATAN
FRD, TODBCD_TO, TO_BCD

Every other name is the usual one. Do not put comments in ladder text: use tag descriptions.

Numbers

Write a REAL with a decimal point, as in 4.0 or defaultValue: 20.0. You can mix DINT and REAL in one calculation. The tag the result goes into decides what is kept: dividing 7 by 2 gives 3.5 into a REAL and 3 into a DINT, even when both inputs are whole numbers.

Structured Text

Structured Text uses FBD_TIMER and FBD_COUNTER rather than TIMER and COUNTER.

  • Timers: set TimerEnable, then call TONR(tag) every scan. TONR clears ACC when TimerEnable goes to 0; use RTOR(tag) for a timer that keeps its time. An FBD_TIMER also has Reset, PRE, ACC, EN, TT and DN.
  • Counters: set CUEnable or CDEnable, then call CTUD(tag) every scan. An FBD_COUNTER also has Reset, PRE, ACC and DN.

Write BOOL values as 1 and 0 in code too, because TRUE does not work. You can assign a whole expression or comparison to a BOOL, as in Tmr_Jam.TimerEnable := In_Running AND In_Sensor; or Out_High := Out_Percent > 75.0;. Declare a loop's counter as a DINT tag. FOR includes its end value. An Output keeps its value on a scan that does not write it. Comments are written // like this or (* like this *).

KindWritten as
Compare=, <>, <, <=, >, >=
Arithmetic+, -, *, /, MOD
LogicAND, OR, XOR, NOT
ChoosingIF … THEN … ELSIF … THEN … ELSE … END_IF; and CASE Wrk_Step OF 1: …; 2, 3: …; ELSE … END_CASE;
RepeatingFOR Wrk_Index := 0 TO 3 DO … END_FOR; and WHILE … DO … END_WHILE;

This is the same lamp in Structured Text. An IF keeps it on after the button is released, until In_Clear is pressed with the button up. What is Structured Text explains the language.

delayed-lamp-st.rungs
version: 6
aoi:
  name: DelayedLampST
  description: Latches the lamp on once the button has been held for 2 seconds.
  parameters:
    - name: In_Button
      usage: input
      dataType: BOOL
    - name: In_Clear
      usage: input
      dataType: BOOL
    - name: Out_Lamp
      usage: output
      dataType: BOOL
  localTags:
    - name: Tmr_Delay
      dataType: FBD_TIMER
      defaultValue:
        PRE: 2000
  routines:
    Logic:
      type: st
      content: |-
        // The lamp latches on, so releasing the button does not turn it off.
        Tmr_Delay.TimerEnable := In_Button;
        TONR(Tmr_Delay);

        IF Tmr_Delay.DN THEN
          Out_Lamp := 1;
        ELSIF In_Clear AND NOT In_Button THEN
          Out_Lamp := 0;
        END_IF;
tests: |
  - name: lamp waits 2 seconds
    steps:
      - in:
          In_Button: 1
        advance:
          time: 1800ms
        expect:
          Out_Lamp: 0
      - advance:
          time: 400ms
        expect:
          Out_Lamp: 1
      - in:
          In_Button: 0
        advance:
          scans: 2
        expect:
          Out_Lamp: 1
      - in:
          In_Clear: 1
        advance:
          scans: 1
        expect:
          Out_Lamp: 0

Tests

tests holds a test suite as a block of text, so write tests: | and indent the suite beneath it. Unit tests describes every field. These are the rules that catch people out:

  • Each test starts fresh. In each step, in applies first, then the scans run, then the checks. Every step needs advance and a check, which is expect or hold.
  • in sets Input and InOut tags, and a value stays set for the following steps. An Input you don't set keeps its defaultValue.
  • expect can name several tags, but only Output and InOut tags. It cannot read a Local.
  • advance takes scans, at least 1, or time, such as 500ms, 2s or 1m.
  • Test scans are 100 ms apart and the first runs at 0 ms, so time: 1s runs 10 scans and ends at 900 ms. Leave a scan or two of margin around a timer's edge.
  • To test a counter or a one-shot, start its input at 0 for one step that expects the unchanged value, then turn it on. A timer can start with its input on.
  • Write negative numbers and decimals as they are, such as -2 and 75.0.

Include tests when you generate a file. They are the quickest way to find out whether the logic does what you meant, because one click on Run tests checks all of it.

Start values for InOut tags

An InOut tag has no default, because the tag belongs to the caller. io sets what Studio, standing in for the caller, holds in the tag when you press Start. value is its only field, and it can be a number, a text or a list. Tests do not read io, so set the same tag with in in a test.

recipe.rungs
version: 6
aoi:
  name: RecipeTotal
  parameters:
    - name: Ref_Recipe
      usage: inout
      dataType: DINT
      dimension: 3
    - name: Out_Total
      usage: output
      dataType: DINT
  routines:
    Logic:
      type: ld
      content: |-
        ADD(Ref_Recipe[0],Ref_Recipe[1],Out_Total);
        ADD(Out_Total,Ref_Recipe[2],Out_Total);
io:
  Ref_Recipe:
    value: [10, 20, 30]
tests: |
  - name: adds up the recipe
    steps:
      - in:
          Ref_Recipe: [1, 2, 3]
        advance:
          scans: 1
        expect:
          Out_Total: 6

A link can carry a whole .rungs file, so someone can open a project with one click and no file to download. This is the quickest way for an AI tool to hand you a program. The project sits after the #, and the browser never sends that part to a server.

There are two forms. Use the first one if you can run code, because the link is about four times shorter.

Compressed. Compress the text of the file with gzip, then write it as base64url with no = padding:

https://studio.rungs.dev/open#<compressed project>
import base64, gzip
project = base64.urlsafe_b64encode(gzip.compress(text.encode())).rstrip(b"=").decode()
const project = require('node:zlib').gzipSync(text).toString('base64url');

Plain text. Put rungs= first, then the text of the file, percent-encoded the way JavaScript's encodeURIComponent does it, then &end. A line break becomes %0A, a space %20 and a colon %3A. This form needs no code, so a chat assistant can write it. The &end lets Studio tell a whole link from one that was cut short.

hello-lamp-link
https://studio.rungs.dev/open#rungs=version%3A%206%0Aaoi%3A%0A%20%20name%3A%20HelloLamp%0A%20%20parameters%3A%0A%20%20%20%20-%20name%3A%20In_Button%0A%20%20%20%20%20%20usage%3A%20input%0A%20%20%20%20%20%20dataType%3A%20BOOL%0A%20%20%20%20-%20name%3A%20Out_Lamp%0A%20%20%20%20%20%20usage%3A%20output%0A%20%20%20%20%20%20dataType%3A%20BOOL%0A%20%20routines%3A%0A%20%20%20%20Logic%3A%0A%20%20%20%20%20%20type%3A%20ld%0A%20%20%20%20%20%20content%3A%20XIC(In_Button)%20OTE(Out_Lamp)%3B%0A&end

Always include the version line. It tells Studio how to read the project. A link can unpack to at most 1 MB.

A project link is for handing over a project once, such as an AI tool giving you a program to open. It holds a copy of the project at that moment, so it never picks up later changes. To send a project to other people, or to keep a link to it, use Share, which gives you a short link. To keep your work, save it as a .rungs file.

If a project is already open, Studio asks before replacing it. Studio takes the project out of the address when it opens, so reloading does not bring it back: save your work as usual. Some chat and email apps cut long links. If a link is cut, Studio says it is damaged and changes nothing, so send the .rungs file instead.

When something is wrong

A mistake shows up in one of three ways.

The file does not open. Studio shows the reason and keeps what you have open. This happens when:

  • version is higher than 6, or aoi or its name is missing
  • the YAML is not valid, usually because of wrong indentation
  • the AOI name or a tag name breaks the rules above, or two tags share a name
  • a data type is not allowed for that kind of tag, or an Input or Output has a dimension
  • a section has the wrong shape, such as logic in place of Logic, or parameters that is not a list

The file opens, but the logic has errors. Studio shows these in the editor and in the status panel, and Run tests fails until they are fixed. This happens when a routine uses a tag that does not exist or an instruction Studio does not know. A rung is missing. This is the easiest one to miss. A ladder rung Studio cannot read at all, such as one with an unclosed bracket, is left out when the file opens, and the log says which one. The tests still run, on a shorter program, and can pass. So always read the log after opening a file you generated.

To check a file, open it in Studio, read the log, and click Run tests. If you are working with an AI tool, paste the messages back to it so it can correct the file. When Studio refuses a file or link, the copy button in the dialog copies the message and the technical report, ready to paste.

On this page