Replacing ScriptFunction with JythonElement in Ignition 8.3

Ryan Tanaka8 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Your module builds against 8.1, then fails as soon as you point it at the 8.3 SDK. The compiler cannot resolve com.inductiveautomation.ignition.common.script.hints.ScriptFunction. Inductive Automation removed that annotation in 8.3.0 without a deprecation period.

The fix has three parts:

  • Replace every @ScriptFunction with @JythonElement. Usage is the same, including docBundlePrefix.
  • Leave your @ScriptArg parameter annotations alone.
  • Maintain a separate 8.3 branch. One source tree will not compile against both 8.1 and 8.3.

Read the Build Failure Before You Touch Code

Start here. Sort the errors by type. The annotation error shows up first, but in a real module it is rarely the only one.

Symptom Cause First action
Import of ...script.hints.ScriptFunction fails, or cannot find symbol on @ScriptFunction Annotation removed in 8.3.0 and replaced by @JythonElement Swap the annotation. Keep the attribute values.
Errors reported on @ScriptArg lines Not the annotation removal. @ScriptArg is still supported in 8.3. Confirm the build is resolving the 8.3 SDK artifacts. Then check the API change notes for that specific error.
Annotation fixed, but dozens of errors in unrelated classes Wider 8.3 SDK API changes. This is normal for any non-trivial module. Work through them against the 8.3 migration guide and API change document.
A static final build flag or Gradle property still leaves one target broken Java has no conditional compilation of imports or annotations Stop. Branch the code.
A module built against 8.1 is installed on an 8.3 gateway and fails to load, or its script functions are missing The binary was compiled against the wrong SDK Rebuild from the 8.3 branch.

Understand Why One Annotation Breaks the Whole Build

Annotations are types. javac must resolve every imported class and every annotation type on the compile classpath before it compiles anything else in the file. There is no preprocessor, so there is no Java equivalent of #ifdef.

A static final boolean flag does not help:

  • It can guard statements inside a method body. The compiler strips dead branches after type checking.
  • It cannot guard an import line or an annotation on a method declaration.
  • If ScriptFunction is missing from the 8.3 classpath, the file fails no matter what the flag says.
  • If JythonElement is missing from the 8.1 classpath, the same thing happens in reverse.

On the runtime side, Ignition's scripting layer inspects your module's script classes. It uses these annotations to expose methods to Jython and to pull descriptions from your resource bundle through docBundlePrefix. @ScriptArg carries the parameter names that appear in the Designer.

Swap the method-level annotation and keep the parameter-level annotations, and the discovery path works the same way it did in 8.1.

Do not wait for a deprecated compatibility shim. Inductive Automation makes no long-term SDK compatibility guarantees, not even within an LTS series. Even if a deprecated @ScriptFunction came back in a later 8.3 release, it would be removed with the rest of the deprecated API at the next major release. SDK-breaking changes have already landed at 7.9, 8.0, 8.1, and now 8.3. Plan for a branch per major version.

Swap @ScriptFunction for @JythonElement

This is the mechanical part. Do it on the 8.3 branch only.

  1. Create the 8.3 branch from your current 8.1 mainline. Leave the 8.1 line untouched.
  2. On the new branch, change your Gradle build to pull the 8.3 SDK artifacts instead of 8.1.
  3. Delete the import com.inductiveautomation.ignition.common.script.hints.ScriptFunction; line from every script class.
  4. Add the JythonElement import. Let your IDE resolve it from the 8.3 SDK on the classpath rather than typing a package path by hand.
  5. Replace each @ScriptFunction(...) with @JythonElement(...). Keep the same arguments, including docBundlePrefix.
  6. Leave every @ScriptArg as it is.
  7. Search the whole tree for ScriptFunction, including test sources and any generated or shared code. Remove every remaining reference.
  8. Rebuild. Clear the remaining errors against the 8.3 migration guide and API change document. During early access, Inductive Automation published the API change document as a PDF alongside the 8.3 release announcement. Use that copy if the linked Google Doc asks you for access.

Before, on the 8.1 branch:

import com.inductiveautomation.ignition.common.script.hints.ScriptFunction;

@ScriptFunction(docBundlePrefix = docBundleName)
public PyObject enterBadge(
        @ScriptArg("assetId") PyObject assetId,
        @ScriptArg("badgeNum") PyObject badgeNum,
        @ScriptArg("logIn") PyObject logIn,
        @ScriptArg("tStamp") PyObject tStamp)
        throws JythonExecException, ScriptManager.UndefinedFunctionException {
    return enterBadgeI(assetId, badgeNum, logIn, tStamp);
}

After, on the 8.3 branch:

// ScriptFunction import removed
// JythonElement import: resolve from the 8.3 SDK in your IDE

@JythonElement(docBundlePrefix = docBundleName)
public PyObject enterBadge(
        @ScriptArg("assetId") PyObject assetId,
        @ScriptArg("badgeNum") PyObject badgeNum,
        @ScriptArg("logIn") PyObject logIn,
        @ScriptArg("tStamp") PyObject tStamp)
        throws JythonExecException, ScriptManager.UndefinedFunctionException {
    return enterBadgeI(assetId, badgeNum, logIn, tStamp);
}

Check the throws clause and the ScriptManager.UndefinedFunctionException reference against the 8.3 API change notes too. The annotation swap does not guarantee that every other type in the signature survived unchanged.

Branch the Build Instead of Flagging It

One jar for both platforms is not achievable for a non-trivial module. Build flags waste time, so skip them. Run two lines:

  • 8.1 branch: builds against the 8.1 SDK and ships to 8.1 gateways. It keeps @ScriptFunction.
  • 8.3 branch: builds against the 8.3 SDK and ships to 8.3 gateways. It uses @JythonElement and carries every other 8.3 API change.

Expect to run both lines for as long as customers stay on 8.1. Moving fixes between them with git cherry-pick works well if you keep commits disciplined.

You could split per-version source directories inside one Gradle project. That only works if the rest of the module compiles against both SDKs, and for anything beyond a trivial module it will not. A branch is simpler to reason about and simpler to release from.

Structure the Code So Cherry-Picks Stay Clean

Arrange the code so that version-specific changes and business logic never share a commit.

  • Keep annotated methods thin. Put all logic in an unannotated implementation method, as in the enterBadge → enterBadgeI split above. The annotated wrapper is the only part that differs between branches.
  • Separate commit types. Commit logic fixes to the implementation methods on their own. Commit wrapper or annotation changes on their own. Logic-only commits then cherry-pick without conflicts.
  • Isolate SDK-facing calls. Keep calls into changed SDK APIs in a small set of classes. Keep pure Java/Jython-type logic elsewhere. The smaller the SDK-facing layer, the smaller the diff between branches.
  • Freeze doc bundle keys. Use the same resource bundle keys on both branches so function descriptions stay aligned.
  • Tag releases per branch. Use distinct version strings for 8.1 and 8.3 builds so nobody installs the wrong jar on a gateway.

Verify the Migrated Module

A clean compile is not proof. Test on a real gateway.

  1. Run a clean build on the 8.3 branch. Confirm a full-text search returns zero hits for ScriptFunction.
  2. Install the module on an 8.3 test gateway. Confirm the module reports running and the gateway log shows no errors during module startup.
  3. Open the Designer Script Console and call each migrated function through your module's scripting namespace with known inputs. Compare the results with the 8.1 build.
  4. Check script editor autocomplete:
    • Each function appears.
    • Parameter names match the @ScriptArg values.
    • Descriptions load from the bundle.
    Missing descriptions point to a wrong docBundlePrefix or missing bundle keys, not the annotation swap itself.
  5. Exercise every call path the module exposes, including gateway-scoped and client/Designer-scoped scripts if your module registers in more than one scope.
  6. Rebuild the 8.1 branch and install it on an 8.1 gateway. Confirm the branch split has not disturbed the 8.1 build.

Avoid the Traps That Waste a Day

  • Chasing a single-jar build. That is not the fault to fix. Branch first, then migrate.
  • Waiting for a deprecated shim. Even if one appears, it disappears at the next major release. Your branch strategy does not change.
  • Treating the annotation as the whole job. @ScriptFunction is usually the first error, not the last. Budget time for the rest of the 8.3 API changes.
  • Rewriting @ScriptArg. It is still supported. Leave it alone.
  • Dropping an 8.1 jar on an 8.3 gateway. The module was compiled against a different SDK. Rebuild from the 8.3 branch.
  • Relying on LTS for SDK stability. LTS covers the platform, not your module's binary compatibility. Rebuild and retest on every release you ship against.
  • Mixing logic and annotation edits in one commit. Cherry-picks will conflict every time.

FAQ

Can I compile one Ignition module jar for both 8.1 and 8.3?

Not for a non-trivial module. Java cannot conditionally compile imports or annotations. @ScriptFunction exists only in the 8.1 SDK and @JythonElement only in the 8.3 SDK, and other 8.3 API changes widen the gap. Maintain a separate 8.3 branch and cherry-pick fixes between branches.

Does @ScriptArg still work in Ignition 8.3?

Yes. @ScriptArg is still supported. Only the method-level @ScriptFunction was replaced by @JythonElement, and that replacement uses the same arguments, including docBundlePrefix.

Can I install a module built against the 8.1 SDK on an 8.3 gateway?

No. Rebuild it from an 8.3 branch against the 8.3 SDK. A module compiled against 8.1 references removed APIs such as ScriptFunction and will fail to load or will lose its script functions.

Does Inductive Automation deprecate SDK APIs before removing them?

Not reliably. There is no long-term SDK compatibility guarantee, even within an LTS series. ScriptFunction was replaced outright in 8.3.0, and anything that is deprecated is removed at the next major release. Plan for one branch per major version.

Escalate to Inductive Automation through its official support channel or developer documentation in two cases: the 8.3 API change document is not accessible to you, or a migrated function compiles cleanly but still does not appear on an 8.3 gateway. Include the SDK version you built against, the gateway version, and the module startup section of the gateway log.

Back to blog