If you are coming to Pine Script from Python, JavaScript, or almost any other language, the first few hours are quietly confusing. You declare a variable, assign it a value, and on the next bar it is somehow back to its starting point. You write a loop and it does not do what you expect. You see the keyword var in example code and have no idea why it exists. None of this is explained by normal programming intuition, because Pine Script does not run the way normal languages run.
Once the execution model clicks, most of the language stops being mysterious. The type-qualifier errors, the repainting problems, the reason var and varip exist, and the behavior of the [] history operator all follow from one idea. This article is about that one idea, taught from the ground up.
Your script runs once per bar, not once total
In a normal program, your code runs top to bottom one time. In Pine Script, on a chart with 5,000 bars of history, your entire script runs 5,000 times: once for each bar, left to right, from the oldest bar to the most recent. Every calculation, every if, every plot statement executes again on each bar. This is the loop you never wrote. TradingView is running it for you.
That means a line like myValue = close * 2 is not computed once. It is computed on every bar, and on each bar close refers to that specific bar's closing price. When the script finishes bar 1, it moves to bar 2 and starts over from the top with fresh values. This is why a plain variable appears to reset: it is not resetting, it is being declared and assigned again from scratch on the new bar.
//@version=6
indicator("Runs once per bar", overlay = true)
// This line executes on EVERY bar, not once.
// On each bar, "close" is that bar's close.
doubled = close * 2
plot(doubled)Every variable is really a series of values
Here is the mental model that makes everything else make sense. A variable in Pine Script is not a single value. It is a whole timeline of values, one entry per bar, called a series. When you write close, you are referring to the current bar's value in the closing-price series. The previous bar's close still exists, and you can reach it.
The tool for reaching back in time is the history-referencing operator, written as square brackets. close[1] is the close one bar ago. close[2] is two bars ago. close[0] is the current bar and is the same as writing close. This works for any series you create, not just built-in ones, which is what makes the language feel so natural once it clicks. You are always standing on the current bar, looking back over a timeline.
//@version=6
indicator("Series and history", overlay = true)
// Detect a bar that closed higher than the previous bar
higherClose = close > close[1]
// Your own series can be referenced back in time too
myAvg = (close + close[1] + close[2]) / 3
plotchar(higherClose, "up", "▲", location.belowbar)
plot(myAvg)This is why so many built-in functions "just know" about previous bars. ta.sma(close, 20) does not need you to pass it the last twenty values. It reaches back through the close series itself. The series model is the whole point of the language, and it is the thing generic code generators most often get wrong, because they treat Pine variables like ordinary scalars.
Why var exists: keeping a value across bars
If a normal variable is re-declared and reassigned on every bar, how do you keep a running total, a counter, or a value that should survive from one bar to the next? That is exactly the problem var solves. A variable declared with var is initialized only once, on the first bar the script runs, and then keeps its value across every following bar until you explicitly change it.
Compare the two. Without var, the counter is reset to zero on every bar, so it can never climb above one. With var, the zero assignment happens a single time and the increment accumulates bar after bar.
//@version=6
indicator("Why var exists")
// WRONG: resets to 0 every bar, so it never counts up
counterA = 0
counterA := counterA + 1 // always 1
// RIGHT: initialized once, persists across bars
var int counterB = 0
counterB := counterB + 1 // 1, 2, 3, 4, ... bar by bar
plot(counterA, "no var", color.red)
plot(counterB, "with var", color.green)Notice the := operator. In Pine Script, = declares a new variable, while := reassigns an existing one. Trying to use = twice on the same name is a common early error. Once you understand that the script reruns each bar, the split makes sense: you declare once, then reassign on subsequent passes.
Historical bars versus the realtime bar
So far we have described how the script moves across completed, historical bars. The last bar on the chart, the one still forming right now, behaves differently, and this difference is the root of most repainting confusion.
On historical bars, each bar ran once and its values are frozen. On the realtime bar, the script re-executes on every incoming price update, or tick. Each time it reruns, it first rolls back any changes it made on the previous tick, then runs again with the new price. So a value you calculate on the realtime bar can flicker up and down as the bar forms, and only settles when the bar closes. The variable barstate.isconfirmed is true only on that final, closing tick, which is why it is the key to writing signals that do not repaint.
| Historical bars | Realtime bar | |
|---|---|---|
| When it runs | Once, at load | On every price update |
| Recalculates? | No, values are fixed | Yes, rolls back and reruns each tick |
| barstate.isconfirmed | Always true | True only on the final tick |
| var value | Set once, then persists | Persists, but can be rewritten mid-bar |
| varip value | Same as var | Updates every tick, never rolled back |
This table is worth rereading. The gap between "ran once and froze" and "reruns every tick and rolls back" explains why a strategy can look perfect on historical bars and then behave differently live. If you want the full treatment of that problem, see our guide to the five kinds of repainting, and for higher-timeframe data specifically, the guide to multi-timeframe analysis without repainting.
varip: state that survives ticks, not just bars
There is a second persistence keyword, varip (var intrabar persist). Like var, it initializes once. Unlike var, its value is not rolled back between ticks on the realtime bar. That makes it the right tool when you genuinely need to accumulate something on every tick, such as counting price updates within a bar or tracking the highest price seen intrabar. It is the wrong tool almost everywhere else, because most logic wants to reason about confirmed bars, not raw ticks. We cover the distinction in depth in the full var and varip guide.
Why loops feel strange
New users often reach for a for loop to "go back over the last twenty bars," then get confused when it is slow or behaves oddly. The reason is that you rarely need a loop for that at all. The script is already looping over bars for you, and the [] operator already reaches back in time. A loop inside a single bar is for something different: iterating over a fixed range of values on that one bar, like summing an array or scanning a set of levels.
Loops also carry a hard constraint. The execution time for any loop on a single bar is capped at 500 milliseconds, and if you exceed it the script errors out. Because the loop runs on every bar, a loop that is even slightly expensive multiplies across thousands of bars. This is one of the most common causes of slow scripts, which we break down in why your Pine Script is slow.
Putting it together: a worked example
Say you want to count how many of the last ten bars closed green, and flag when at least seven did. A newcomer might try to store history in a variable and loop over bars. The idiomatic Pine version leans on the series model instead.
//@version=6
indicator("Green bars in last 10", overlay = true)
isGreen = close > open
// math.sum adds the last N values of a series.
// isGreen is a series of 1s and 0s across bars.
greenCount = math.sum(isGreen ? 1 : 0, 10)
strongUp = greenCount >= 7
bgcolor(strongUp ? color.new(color.green, 85) : na)
plot(greenCount, "green count")No manual loop, no manual history storage. isGreen is a series that exists on every bar, and math.sum walks back through it. This is what Pine code looks like when it works with the execution model rather than against it.
Why this matters if you use AI to write Pine Script
Almost every failure mode of generic AI-written Pine Script traces back to the execution model. A general model like ChatGPT treats variables as ordinary scalars, so it forgets that a plain variable resets each bar, misuses or omits var, reaches for loops where the series model would do, and produces code that quietly repaints on the realtime bar. It has no way to feel the once-per-bar rerun, because it has never watched the code run.
This is the specific thing a purpose-built tool handles differently. PineScripter has the Pine Script v5 and v6 manuals loaded as context and generates code that respects the series model and the historical-versus-realtime distinction by default, then runs it through a validation loop that catches the errors that slip past generic models. You can see the generation workflow in how PineScripter works. If you would rather write it yourself, understanding this article is most of the battle.
The short version to carry with you: your script is a loop over bars, every variable is a timeline, [] reaches back in time, var keeps a value across bars, and the realtime bar reruns on every tick. Hold those five facts and the rest of Pine Script becomes readable. When you are ready to turn an idea into working code without fighting the syntax, you can try it free at PineScripter.
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.