EyeAuras scripts are regular C# code that runs inside the EyeAuras runtime environment. They work well both for tiny 20-line utilities and for larger solutions that can later grow into a pack or a mini-app.
Script.csx vs regular classesThis is one of the most important concepts in the whole system.
Code inside Script.csx goes through special EyeAuras processing:
Log, Sleep(...), GetService<T>(), AuraTree, Variables, cancellationTokenFor example, inside Script.csx itself, you can write this:
Log.Info("Script started");
Sleep(100);
var input = GetService<ISendInputScriptingApi>();
input.KeyPress(Key.F);
But helper classes that sit next to the script are just regular C#. They do not get any of that “magic” direct access to the sandbox.
If you write a separate class, it cannot just call Log or AuraTree on its own:
public sealed class Helper
{
private readonly IFluentLog log;
public Helper(IFluentLog log)
{
this.log = log;
}
public void DoWork()
{
log.Info("Helper is working");
}
}
So the rule is simple:
Script.csx is a special EyeAuras entry pointGetService<T>()This is important for both humans and AI: not every part of the project has the same “magic” capabilities.
The short answer: scripts are useful not because “writing software is hard,” but because EyeAuras is already a ready-to-use automation platform.
If you build a standalone program from scratch, you have to assemble a lot of infrastructure around your logic yourself:
In EyeAuras, most of that infrastructure already exists. That means you only write your own logic and can immediately use ready-made auras, triggers, variables, CV API, input, and UI.
That gives you several practical advantages:
.sln, then into a pack, then into a mini-appIf your project does not really use EyeAuras capabilities and does not depend on its runtime environment, then a regular standalone .NET application may be the more direct choice.
From the language perspective, it is regular C# code on .NET and Roslyn. From the product perspective, it is code that runs inside the sandbox and gets access to the EyeAuras API: logs, variables, auras, input, CV, UI, and other services.
Read more: Getting started, Sandbox
Examples:
Before you decide to build a separate program, it helps to understand what EyeAuras already provides:
.sln export/importIf you are building a private or commercial solution, licensing, key login, packs, and code protection can save a lot of time.
Keybind, and how it compares to AutoHotkey[Keybind] lets you attach a hotkey handler directly to a script method.
If you have used AutoHotkey before, the idea is very similar:
F1:: in AHK is similar to [Keybind("F1")]*F1:: is similar to [Keybind("F1", IgnoreModifiers = true)]~F1:: is similar to [Keybind("F1", SuppressKey = false)]d up:: is similar to [Keybind("D", ActivationType = KeybindActivationType.KeyUp)]Minimal examples:
[Keybind("F1")]
public void OnF1()
{
Log.Info("F1 pressed");
}
[Keybind("F1", IgnoreModifiers = true)]
public void OnF1WithAnyModifiers()
{
Log.Info("Works for F1, Shift+F1, Ctrl+F1");
}
[Keybind("D", ActivationType = KeybindActivationType.KeyUp)]
public void OnDReleased()
{
Log.Info("D released");
}
Why this is useful in practice:
Two important details:
If you need to block a second handler until the first one finishes, you can do this:
System.Threading.SemaphoreSlim hotkeyGate = new(1, 1);
[Keybind("F2")]
public void RunSingleHotkey()
{
if (!hotkeyGate.Wait(0))
{
Log.Info("Hotkey is already being processed");
return;
}
try
{
Log.Info("Handling F2");
Sleep(300);
}
finally
{
hotkeyGate.Release();
}
}
A small useful bonus: a keybind handler can also take dependencies as method parameters if they are already available through DI.
If the hotkey should be visible in the aura tree, combined with conditions, and live as part of the overall visual logic, then using the HotkeyIsActive trigger is often more convenient than [Keybind].
Read more: Hotkeys
Examples:
For UI in EyeAuras, there are usually two main paths: Blazor Windows and ImGui.
ImGui is usually the easiest place to start.
Script.csxSleep(...) or heavy code inside itAddRenderer(...), and inside the render method you should only use the ready stateThis is a good choice if you want to build a working UI quickly without creating many files.
Blazor Windows is usually more structured, but much richer visually.
Script.csx, *.razor, *.cs*.razor and code in *.cs, because it is easier to read and IntelliSense is currently better thereScript.csx usually only creates the window or overlay, and the state then lives inside the component itselfScript.csx is more of a window creation entry point than the place that “renders UI every frame”OnInitializedAsync, while JavaScript that needs an existing DOM is usually loaded in OnAfterFirstRenderAsyncIn practice, that means Blazor usually needs a bit more structure, but the result can feel much more like a finished product.
Short version:
ImGui is simpler, faster, and closer to “everything in one file”Blazor Windows is more complex, but prettier and more flexibleOne more important distinction:
Script.csx usually acts as the entry point, while the real UI lives in Razor componentsAddRenderer(...) methodMinimal Blazor lifecycle example:
[Inject] public PoeShared.Blazor.Services.IJsPoeBlazorUtils PoeBlazorUtils { get; init; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await PoeBlazorUtils.LoadCss("Styles.css");
}
protected override async Task OnAfterFirstRenderAsync()
{
await base.OnAfterFirstRenderAsync();
await PoeBlazorUtils.LoadScript("Script.js");
}
Read more: Blazor Windows, ImGui getting started
Examples:
Script.csxThere are a few small syntax details that often surprise people, because Script.csx is not compiled like a regular .cs file. It is compiled as a Roslyn submission script.
using Namespace; and using for IDisposable are different thingsNormal namespace directives work as usual:
using System.Text;
using OpenQA.Selenium;
But resource disposal constructs at the top level of Script.csx require a bit more care.
using, it is better to wrap it in its own scopeIf you want to dispose resources directly at the top level of Script.csx, it is best to create an explicit scope with {}.
This applies to both using var and using (...) { ... }.
Simplest version:
{
using var canvas = GetService<IOnScreenCanvasScriptingApi>().Create();
Log.Info("Canvas created");
}
Or like this:
{
using (var stream = GetService<IScriptFileProvider>().GetFileInfo("docs/help.md").CreateReadStream())
{
Log.Info("Stream opened");
}
}
Why:
Script.csx is compiled in a special wayInside normal methods, local functions, try, if, while, and other blocks, using usually works without issues.
Practical rule:
using Some.Namespace; at the top of the file as usualusing var resource = ... at the top level of Script.csx inside { ... }using (...) { ... } at the top level of Script.csx inside an outer { ... }try { ... }, a separate scope is usually unnecessary because you already have oneExamples:
async/await freelyYou can use async/await normally in Script.csx.
For example:
var text = await File.ReadAllTextAsync("input.txt");
Log.Info(text);
Or this:
async Task<int> LoadValue()
{
await Task.Delay(100);
return 42;
}
var value = await LoadValue();
Log.Info(value);
In practice, this means:
await, the compiler adapts the signature automaticallyawaitTask<T>So in day-to-day use, you can think about it very simply:
await, just use awaitreturn the valueOne extra recommendation: avoid async void where possible and prefer async Task, even though some internal EyeAuras rewriters can automatically patch such cases.
This is another frequent source of confusion.
Inside EyeAuras, Script.csx does not always behave as if it were fully recreated for every invocation. If the script was not recompiled and its runtime context stayed the same, some top-level state can survive repeated runs.
In practice, this means:
Variables is still the better choiceA good practical rule:
VariablesThe sandbox is the environment where your script runs. It:
Log, Sleep(...), GetService<T>()In short, the sandbox is the host you do not have to write yourself.
Read more: Sandbox
There are three main options:
[Inject] + init: a good choice for core dependencies that are set once at startup[Dependency] + set: useful when the dependency should be a property that can be replaced if neededGetService<T>(): when you need the service right here, right nowMinimal examples:
[Inject] public IAuraTreeScriptingApi AuraTree { get; init; }
[Dependency] public ISendInputScriptingApi SendInput { get; set; }
var input = GetService<ISendInputScriptingApi>();
If you are just getting started, GetService<T>() is usually the clearest option. In larger codebases, properties with [Inject] or [Dependency] often make the script cleaner.
Read more: Dependency Injection, Script Container Extensions
Examples:
cancellationToken, and where does it come fromcancellationToken is automatically available in every script. EyeAuras passes it into the sandbox before the script starts.
Very briefly, CancellationToken in .NET is the standard object used to tell code: “it is time to stop.” Official docs: CancellationToken.
In practice, this means:
Sleep(...) finishes earlyImportant: EyeAuras can inject some cancellation logic automatically, but in real code it is still better to write explicit, readable, cancellation-aware logic yourself.
There are two main scenarios.
If you have already created an overlay, window, renderer, subscription, or another background object, and the script does not need active polling after that, this is usually enough:
cancellationToken.WaitHandle.WaitOne();
This is a good fit for:
The main advantage is that it does not spin an empty loop or waste CPU for no reason.
If you have an active loop, use an explicit cancellation check:
while (!cancellationToken.IsCancellationRequested)
{
DoSomething();
Sleep(50);
}
This is a good fit for:
Important points:
Sleep(...) unless you have a very good reasonSleep(...) in EyeAuras already knows how to finish early when the script stopsShort version:
WaitOne() for “everything is already running, just stay alive until stop”while (!cancellationToken.IsCancellationRequested) for “keep doing work while alive”Examples:
Sleep(...) is better than Thread.Sleep(...) in scriptsIn Script.csx, it is almost always better to use sandbox Sleep(...).
Why:
Sleep(...) is tied to cancellationToken and can finish early on stopThread.Sleep(...) in Script.csx is additionally rewritten by a rewriter into Sleep(...)Read more: Sleep() rework
Below is a comparison image showing the precision of different waiting methods:

Practical rule:
Script.csx, write Sleep(...)Thread.Sleep(...)Task.Delay(...) only if you specifically need the async model and understand whyScript.csxEyeAuras applies code rewriters to Script.csx. This is another reason why it is important to separate script code from regular classes.
In practice, at minimum, it helps to remember this:
while (true) by adding a cancellationToken checkThread.Sleep(...) in script code is replaced with Sleep(...)CancellationToken may be automatically adjusted by the runtimeThis is a useful safety net, but not a reason to write sloppy code. The best practice is still:
while (!cancellationToken.IsCancellationRequested) yourselfSleep(...) yourselfAnchors, ExecutionAnchors, and disposable resources forIn EyeAuras, it helps to think not only “what object did I create,” but also “when should it die.”
If an object needs proper cleanup later, it is usually attached to a CompositeDisposable. Official reference: CompositeDisposable.
The important lifecycle levels are:
Anchors are the anchors of the sandbox or object. If you add a resource there, it lives as long as that sandbox or object livesExecutionAnchors are the anchors of the current script run. Stopping and starting the script recreates themCompositeDisposable is useful inside your helper classes when you want to group resources into one cleanup “bundle”Short rule:
ExecutionAnchorsAnchorsExample:
var overlay = GetService<IImGuiExperimentalApi>()
.AddTo(ExecutionAnchors);
That means the overlay only lives for the current script run.
Examples:
IScriptingApiContextIScriptingApiContext is an internal script context object. It contains:
Solution of the current scriptAnchors for resources in that contextNormally, a regular script author does not need to create IScriptingApiContext manually. But the general idea is useful to understand:
AnchorsThis becomes especially important if you build your own libraries, extensions, or move code from one file into a larger structure.
ExecutionAnchorsIf your script creates resources that should die with the current script run, attach them to ExecutionAnchors.
Typical examples:
The idea is simple: if the resource belongs specifically to the current execution of the script, tie it to the lifetime of that execution.
Examples:
In practice, it helps to think in three levels:
For most scenarios, the best API is ScriptVariable<T> via Variables.Get<T>(...).
var attempts = Variables.Get<int>("attempts");
attempts.Value++;
Advantages of this approach:
objectRead more: Variables, IVariablesScriptingApi
Examples:
A script usually has access to the current aura and current folder:
AuraTree.AuraAuraTree.FolderYou can also find other items by path:
FindAuraByPath(...) / GetAuraByPath(...)FindFolderByPath(...) / GetFolderByPath(...)GetTriggerByPath<T>(...)Useful rule:
Find* when it is normal for the object to be missingGet* when a missing object is an errorImportant: auras also have their own variables. So you can store data not only at the current script level, but also at the aura or folder level.
var aura = AuraTree.GetAuraByPath(@".\Gameplay\Boss");
var phase = Variables.Get<string>(aura, "phase");
Read more: IAuraTreeScriptingApi, IAuraAccessor, How to find an aura
Examples:
AddNewExtension<T>(), and when do you need itSome EyeAuras features are packaged as separate container extensions. This is how you connect a new set of services to the DI container.
For example, some libraries expect explicit activation directly from Script.csx:
AddNewExtension<ImGuiContainerExtensions>();
var imgui = GetService<IImGuiExperimentalApi>();
AddNewExtension<FridaContainerExtensions>();
var frida = GetService<IFridaExperimentalApi>();
This is especially useful for modular packages that you do not want enabled all the time.
Read more: Script Container Extensions, ImGui getting started
Examples:
If a library already exists as a NuGet package, that is usually the best option.
Why:
pack revisions, EyeAuras preloads NuGet dependencies and includes them in the packBasic syntax:
#r "nuget: Some.Package, 1.2.3"
Minimal example:
#r "nuget: Newtonsoft.Json, 13.0.3"
using Newtonsoft.Json;
Log.Info(JsonConvert.SerializeObject(new { Hello = "World" }));
Important: #r directives must be written in Script.csx. They do not work in regular related *.cs classes.
Read more: NuGet and assemblies, NuGet packaging inside packs
Local assemblies are useful when:
The best portable option is usually:
#r "assemblyPath: MyLib.dll"Minimal example:
#r "assemblyPath: MyLib.dll"
using MyLib;
var helper = new SomeLibraryHelper();
Log.Info(helper.ToString());
This is much better than an absolute path like D:\\Work\\Something\\MyLib.dll, because that path only works on the author’s machine.
Use absolute paths mostly for local experiments and debugging. For portable solutions, prefer:
Read more: Embedded resources, NuGet and assemblies
A pack is a portable version of your EyeAuras-based solution. It usually includes:
What matters for scripts:
If a script will run anywhere other than your own machine, a good base rule is:
NuGet > embedded DLL > absolute DLL pathRead more: Packs, NuGet packaging inside packs, resources in packs
A mini-app is no longer just “a script inside EyeAuras,” but your own product built on top of EyeAuras.
Short version:
A mini-app is a good fit when you want to:
Yes. That is one of the main strengths of the whole system.
A typical growth path looks like this:
.csx or C# Action.slnImport / Live Importpackmini-app if neededSo an EyeAuras script is not a dead-end “embedded macro.” It is a normal entry point into a larger project.
Read more: IDE integration, Mini-app
If you are building your own product on top of EyeAuras, there are several related topics:
For ordinary local scripts, you may not need to think about this at all. For commercial or private solutions, it becomes an important part of the architecture.
If a task can be assembled cleanly using ready-made triggers, auras, variables, and actions, that is usually the best place to start. Scripts work especially well as “smart glue” between existing platform pieces.
It is usually simpler and more reliable to:
AuraTreeInstead of writing the entire mechanic manually from scratch.
Even though EyeAuras can automatically help in some places, explicit code is usually better:
Good baseline patterns:
cancellationToken.WaitHandle.WaitOne();while (!cancellationToken.IsCancellationRequested) { ... }Script.csx, and write normal C# in related classesThis rule is simple, but very useful.
In Script.csx, it is perfectly normal to write:
Log.Info(...)Sleep(...)GetService<T>()AuraTree.Get...(...)But in helper classes, it is better to either:
GetService<T>(), so the container can assemble the dependencies for youIf other people will run the solution later, avoid:
D:\\...For portable solutions, it is almost always better to use:
#r "nuget: ..."In older examples and documentation, you may still see ISendInputUnstableScriptingApi, but for new code you should prefer ISendInputScriptingApi.
The old API is mainly useful to know about because you may still encounter it in legacy scripts.
Unstable in an API name usually does not mean “broken” or “dangerous.” It usually only means the interface may still change between versions. If a stable equivalent already exists, prefer it. If the functionality you need only exists in Unstable for now, using it is fine—just be aware of possible breaking changes when updating.
Examples using the newer API:
Variables.Get<T>() for variablesYou can work through the string indexer, but typed access via ScriptVariable<T> is usually much cleaner and safer.
Find* for optional objects, Get* for required onesThis simple rule greatly reduces accidental crashes and makes the code’s intent much clearer.
ExecutionAnchorsThis is especially important for:
That way stop/start behaves predictably and the script leaves less garbage behind.
Examples:
AddNewExtension<T>() for libraries that require separate registrationIf a package explicitly tells you to call AddNewExtension<T>(), that is not just a using. It is real registration of a new set of services in the container.
Typical example:
AddNewExtension<ImGuiContainerExtensions>();
var imgui = GetService<IImGuiExperimentalApi>();
Without registration, the service may simply never appear in the container.
Examples:
.slnIf the code has grown big, do not try to keep everything in one file forever.
A good time to switch is when:
That is exactly what Export / Import / Live Import is for.
One practical detail: Export clears the target folder before exporting the project, so do not export into a directory that contains important files.
If you regularly write or maintain large scripts, it is highly recommended to work in a setup like IDE + AI + EyeAuras MCP. My personal choice right now is Rider + AI Assistant/Codex + EyeAuras MCP, but Visual Studio and VS Code are also perfectly workable options.
Read more: IDE, AI, and MCP integration
For most tasks, the simplest and most reliable approach is to use a configured Image Search trigger and read its result from the script.
var trigger = AuraTree.GetTriggerByPath<IImageSearchTrigger>(@".\ImageSearch");
var result = trigger.Refresh();
if (!result.Detected.Success)
{
Log.Warn("Image not found");
return;
}
var screenRect = result.ToScreen(result.Detected.Bounds.Value);
Log.Info($"Image found @ {screenRect}");
Read more: How to find an image
See also:
For that, you usually need ISendInputScriptingApi:
var input = GetService<ISendInputScriptingApi>();
input.KeyPress(Key.F);
input.MouseLeftClick();
Depending on the task, you can use:
KeyPress(...)KeyDown(...) / KeyUp(...)MouseMoveTo(...)MouseLeftClick() / MouseRightClick()Read more: ISendInputScriptingApi
Examples:
The general pattern is almost always the same:
var input = GetService<ISendInputScriptingApi>();
var trigger = AuraTree.GetTriggerByPath<IImageSearchTrigger>(@".\ImageSearch");
var result = trigger.Refresh();
if (!result.Detected.Success)
{
return;
}
var screenRect = result.ToScreen(result.Detected.Bounds.Value);
input.MouseMoveTo(screenRect.Center());
input.MouseLeftClick();
For text, the logic is the same, just with Text Search as the data source.
Read more: How to click recognized text
See also:
This is one of the most common sources of mistakes.
In EyeAuras, you usually deal with three kinds of coordinates:
The safe rule is simple:
ToScreen(...) or ToScreenPoint(...)Most useful methods:
ToScreen(rect)ToScreenPoint(rect)Center()Read more: WindowImageProcessedEventArgs
Examples:
Embedded resources are useful when you want to ship files together with the script:
Two of the simplest scenarios:
Show an image in Blazor:
<img src="Images/logo.png" />
Read a text file from the script:
var files = GetService<IScriptFileProvider>();
var helpText = files.ReadAllText("docs/help.md");
Log.Info(helpText);
One small practical detail that often saves time: it is better to write Images/logo.png or docs/help.md, not just logo.png or help.md. Short names are convenient, but if path endings match, it is easy to pick up the wrong file.
If a library already exists on NuGet, it is almost always better to use it through #r "nuget: ...". Embedded resources are especially good for your own files and portable DLLs.
Read more: Embedded resources
Examples: