Press enter or click to view image in full size
AI is transforming how software is built, but many of its benefits are still focused on people who can already code. Coding agents make developers more productive because they can write, run, and revise software. Ordinary application users should be able to benefit from a similar kind of transformation without becoming programmers themselves.
An application user often knows the outcome they need: a particular search, report, validation rule, visualization, routing policy, or workflow. They can very well describe that outcome without knowing how to implement it. However, they cannot be expected to study extension points or review code generated on their behalf.
But would you let ordinary users install AI-generated code into a production application without anyone reviewing it? At first glance, the only responsible answer is no, no, and again no! But that assumes generated code must be trusted before it can run.
What if generated code could be useful without being able to perform destructive actions directly?
That is the motivation behind our Graal Script Agent library.
From prompt to result
Graal Script Agent is a library that lets applications turn end-user prompts into sandboxed plugins. The application owner defines the allowed extension points. The model generates each script from the user’s prompt and extension contract without having access to production data. The application can then run it locally whenever needed.
As a first demo, we have added a new magic Script Agent tab to everyone’s favorite example, PetClinic:
Press enter or click to view image in full size
Now put yourself in the position of one of your users. Frank is a veterinarian trying to follow up with a pet owner. He has forgotten the owner’s name, but remembers that the owner lives somewhere in Madison. Let’s assume PetClinic’s built-in search supports owner lookups by last name only, so he is stuck.
Frank is amazing with animals but completely clueless about software. Today is his lucky day because it can stay that way. Instead of learning a complex query screen or a query language, he simply describes what he remembers and the data he wants:
“Show owners in Madison with their telephone numbers.”
And this is the result:
Press enter or click to view image in full size
He gets what he wanted: a list of owners in Madison, with each owner’s telephone number shown alongside their name, perhaps enough to refresh his memory.
What happened here? The agent selected the query extension point and generated a script against the query-only APIs exposed by the application. Because every capability in this extension point is non-destructive, PetClinic can execute the script directly and display its result.
Before we move on, notice the Add to Find Owners page button below the result. Clicking it persists the generated script as a plugin and makes this query available directly from PetClinic’s regular Find Owners page.
Whenever someone runs the saved query, the plugin executes locally against current data without another model call. Production objects stay inside the application, and the functionality remains available until it is removed, without anyone reviewing generated code.
A query is the straightforward case because it cannot change application data. But what if Frank asks PetClinic to make a change?
“Add a dog named Fido born 2022–05–01 for George Franklin and add a visit on 2026–03–01 for an annual checkup.”
Press enter or click to view image in full size
This time, the agent selects a different extension point: one designed for data modifications. The generated script can query owner data and call an API for creating and modifying pets and owners. During script execution, however, the host records the requested modifications instead of committing them and displays the result.
Frank can now review the proposed pet and visit and choose whether to execute the modification. He reviews the domain-specific result, not the generated code. If he presses Execute, PetClinic validates and persists the approved modifications.
The selected extension point sets the security posture: queries can run directly, while modifications require review and approval.
That is the core idea. Now let’s find out how you can build such plugin mechanisms yourself with Graal Script Agent.
Defining the Extension Points
An extension point is a place where an application deliberately allows new behavior to plug in. In Java, it can be expressed as an interface that defines what a plugin must return and which application APIs it may use.
PetClinic’s extension contract can be simplified to this:
sealed interface Extension
permits QueryExtension, ModificationExtension {
}non-sealed interface QueryExtension extends Extension {
QueryResult execute(OwnerQueryApi ownersApi);
}
non-sealed interface ModificationExtension extends Extension {
void execute(OwnerQueryApi ownersApi,
ModificationApi modificationApi);
}
interface OwnerQueryApi {
List<OwnerView> findByCityStartingWith(String cityPrefix);
}
interface ModificationApi {
void addPet(String ownerFirstName, String ownerLastName,
String petName, String petTypeName, String birthDate);
void addVisit(String ownerFirstName, String ownerLastName,
String petName, String visitDate, String description);
}
Extension is the starting point for schema discovery. Its sealed hierarchy defines the available extension shapes, while each execute method defines
the script’s entry point and the application APIs available to it. The child interfaces remain non-sealed so the completed script can be bound to the
selected interface. This single-method shape is probably the most common, although extension contracts may contain multiple methods and richer type hierarchies.
With the extension point defined, generating an extension script only requires a model connector, a guest language, and the user’s prompt:
Model model = /* LangChainModel.of(...) or SpringAiModel.of(...)*/;
String prompt = "Show owners in Madison with their telephone numbers.";Script<Extension> extensionScript;
try (ScriptAgent agent = ScriptAgent.newBuilder(model)
.language("js").build()) {
extensionScript = agent.generate(
Extension.class,
prompt);
}
First, Graal Script Agent discovers the contract starting from Extension, presents it to the model, and asks the model to produce one of its permitted extension types. For Frank’s first prompt, that is a QueryExtension. For his second, it is a ModificationExtension. If the request cannot be implemented against the schema, generation fails with a useful message instead of returning an invalid plugin.
In this example, language(“js”) configures JavaScript as the scripting language. JavaScript is compact and runs locally through embedded GraalJS. Graal Script Agent can also use GraalPy to author Python scripts if preferred.
Against the simplified contract above, the JavaScript generated for Frank’s modification request could look like this:
util.implement(types.ModificationExtension, {
execute(ownersApi, modificationApi) {
modificationApi.addPet("George", "Franklin", "Fido",
"dog", "2022-05-01");
modificationApi.addVisit("George", "Franklin", "Fido",
"2026-03-01", "annual checkup");
}
})The script agent runtime exposes helpers such as util.implement to make it easier for the model to fulfill the contract. The framework handles these internal details. More difficult prompts or larger schemas naturally produce more elaborate scripts, but this shows the basic shape.
This example defines the plugin contract using Java interfaces and lets Graal Script Agent discover it automatically. The extension-point guide explains how to design such contracts in much more detail.
Sandboxing the Plugin
The generated Script<Extension> can be serialized and persisted, but it is not an active plugin. Before the application can use it, the script must be bound to a GraalVM polyglot context for execution:
try (Context context = Sandbox.UNTRUSTED.newContext(
extensionScript.language())) {
Extension extension = extensionScript.bind(context); if (extension instanceof QueryExtension query) {
QueryResult result = query.execute(ownerQueryApi);
display(result);
} else if (extension instanceof ModificationExtension modification) {
ModificationPlan plan = new ModificatinPlan();
modification.execute(plan, ownerQueryApi);
if (reviewByUser(plan)) {
validateAndPersist(plan);
}
}
}
Sandbox.UNTRUSTED treats the generated source as potentially adversarial. It blocks host filesystem, network, process, environment, and arbitrary Java access; runs the guest in an isolated runtime; and limits CPU time, memory, threads, output, and program complexity.
Graal Script Agent secures the boundary between that guest runtime and the application through a schema-driven host binding. The plugin can access only the members and types declared by the extension schema. Undeclared members remain inaccessible even if the underlying Java object exposes more functionality.
Neither layer can make a dangerous exposed API safe. Exposed host methods must still enforce authorization, validation, tenant boundaries, and side-effect policy. For a deeper discussion of sandbox policies, resource limits, isolates, and production considerations, please see our recently published blog post on sandboxing.
Giving the Model Context
Coding agents became dramatically more useful when they got access to a shell. Early assistants mostly suggested code. Modern agents write code, run it, inspect the result, and revise. Code became the universal adapter between the agent and its environment.
Graal Script Agent applies the same idea inside a boundary controlled by the application. Frank’s prompt describes the result he wants, but not PetClinic’s Java API. The generated schema prompt supplies that grounding: it describes the available methods, types, records, enum values, and expected result shapes in language-specific form for JavaScript or Python.
For more complex extensions, Graal Script Agent adds an iterative feedback loop using application-controlled authoring tools rather than shell access:
- Draft editing lets the model build and revise one script.
- Schema inspection provides type details on demand when the API is too large to present in full.
- Sample inspection shows representative synthetic or sanitized application objects to the model and it can use a script to inspect it.
- Mock execution runs the draft in a bounded environment so the model can observe failures and revise it.
- Application-defined tests check the behavior before accepting a plugin and give feedback to the model when a test fails.
Not every extension needs every tool. A clear prompt and a small schema may be enough for the Madison query. Larger or more context-dependent extension points benefit from samples, execution feedback, and tests. When application-defined tests are configured, completion succeeds only after all of them pass.
By default, the model receives the user’s prompt and the schema describing the extension API, not live application data. Sample inspection is an explicit opt-in that makes selected application objects visible to the model. Applications handling sensitive data should use synthetic or sanitized samples, or a local model.
Turn on the music!
As a kid, I was fascinated by Winamp’s music visualizations. I remember staring at them for hours. Creating one myself felt out of reach: too much graphics programming and too much math.
I am much, much older now, but Graal Script Agent lets me create a plugin system where I can prompt my own Winamp visualizations using natural language by exposing an extension point as small as this:
void draw(PixelBuffer buffer, int tick, AudioFrame audio);The generated plugin receives a pixel buffer and a small audio summary on every frame. That tiny contract is enough to generate bars, waves, pulses, beat flashes, and spectrum effects without exposing the UI toolkit, audio system, files, or threads.
Now let’s prompt it with “two rotating dodecahedra”:
The model is not part of the rendering loop; only the generated scripts are. These scripts run locally, filling a 320 × 180 pixel buffer while the demo targets 30 frames per second. GraalJS compiles the hot drawing function, so the visualization runs smoothly even inside the sandbox.
The source code is available here.
Filtering compiler graphs!
We also integrated Graal Script Agent into Ideal Graph Visualizer (IGV), a tool the GraalVM team uses to inspect compiler graphs. These graphs can be very large, so we often write custom filters by hand to find the few nodes or paths relevant to an investigation. With Graal Script Agent, we can instead describe what we want to find or highlight, and it generates the corresponding filter plugin.
IGV demonstrates the opposite end of the spectrum from the tiny Winamp contract. It exposes a substantial graph API, while the concrete node classes, properties, and relationships vary with the compiler graph being inspected. The static schema describes the stable graph operations, but the exact shape of the data becomes clear only at authoring time. Sample inspection lets the model examine representative graphs and ground its generated filters in observed node names, properties, dependencies, block structure, and execution frequencies instead of guessing from generic conventions.
Below is a screenshot of IGV, filtering the most common control flow path of the graph. This is a very useful extension for our workflow.
The source code for this integration will be available soon.
Where It Fits and Where It Doesn’t
Some behaviors should not be exposed as prompt-authored extensions at all. Core price calculations, authorization decisions, or irreversible bulk operations may require stronger assurance and a regular development process.
The best candidates are bounded customizations such as saved searches, ad-hoc reports, routing rules, UI filters, dashboard cards, visualizations, data exploration, and validation logic. They are often specific to one user, team, tenant, or task, valuable enough to matter, but too specialized to justify development.
Extensions can also propose modifications. The application then needs a stronger workflow: review the proposed operations, present them in domain terms, and require confirmation before enabling writes. To be honest, we haven’t fully explored this space, so we don’t exactly know where the limits are.
Not every useful extension needs to become a permanent plugin. Often the most value comes from complex one-off tasks: the user knows the outcome they need but not how to achieve it through the application’s existing screens and controls. The generated extension can run once and then be discarded.
Like a chatbot, the interaction begins with a natural-language request. Unlike a generic chatbot, the result is integrated with the application’s types, data, UI, and safety workflow. It can perform the task through application-defined APIs and present the result where the user already works.
Try it yourself
Graal Script Agent is in preview, but it is ready for you to try!
The docs include a getting-started guide with a minimal application covering schema discovery, script generation, sandboxed execution, and authoring tools.
Start small. Define one query-only Java interface. Expose only methods that you’re comfortable allowing untrusted code to call. Then prompt the agent to implement it and run the result inside an appropriately configured sandbox.
We have built only a handful of applications with this so far, which is why feedback matters a lot at this stage. If prompt-authored extensions sound useful for something you are building, get in touch and join the #script-agent channel in our community Slack. Feel free to open any issues you encounter on GitHub.
Further reading
- Graal Script Agent manual: Start here for setup, schema discovery, runtime binding, and authoring tools
- Blog Post on Sandboxing Script Extensions with GraalVM: Find out more about sandbox policies, resource limits, isolates, and production considerations.
- Micronaut PetClinic Script Agent demo: The demo shown at the beginning.
- Want to contribute? Check out the GitHub repository.