# Custom Functions

Source: https://orbitalhq.com/docs/extending/custom-functions

Orbital lets you extend the platform with **custom functions**, written in [Kotlin](https://kotlinlang.org/) or Java.  

These functions behave just like built-in functions: you can call them from Taxi queries, use them in transformations, and reuse them across projects.

## Why custom functions?

While Taxi’s [standard library](https://taxilang.org/docs/language/stdlib) covers common operations, sometimes you need:

- Custom domain-specific calculations
- Utility functions for reuse across queries

Orbital supports two ways of writing custom functions:

- **Precompiled JARs** (Java or Kotlin)  
- **Kotlin scripts** (`.taxi.kts` files)

**Note: Choosing between JAR files vs Kotlin scripts**
Use JARs if you want strong CI/CD integration, test coverage, and debugger support.  
Use scripts if you want to iterate quickly, without a compile step.

## Writing custom functions
The process for writing a function is the same, regardless if you're using Kotlin or Java, or which method you choose for pacakging your functions.

The steps are as follows:

 1. Implement the `TaxiFunctionProvider` interface (`com.orbitalhq.functions.TaxiFunctionProvider`)
     - `TaxiFunctionProvider` is a marker interface, with no methods, but is used to find the classes that have custom functions
     - If you're using [Kotlin Scrpting](#writing-functions-in-kotlin-scripts) the you can skip this - the scripting engine handles this for you.
 2. Annotate your functions with `@TaxiFunction`
 3. Annotate parameters with `@TaxiParam`

Here's an example:

```kotlin
// Always put your functions in a package - this will 
// dictate the namespace that the corresponding Taxi function is declared under
package com.orbitalhq.example

import com.orbitalhq.functions.TaxiFunction
import com.orbitalhq.functions.TaxiFunctionProvider
import com.orbitalhq.functions.TaxiParam

class ExampleFunctions : TaxiFunctionProvider { // <-- implement TaxiFunctionProvider

   @TaxiFunction // <-- annotation
   fun sayHello(@TaxiParam name: String):String { 
      return "Hello, $name"
   }
}
```

The function declarations are fairly vanilla. Orbital handles the marshalling into and out of your function for you,
as well as generating the corresponding Taxi [function definitions](https://taxilang.org/docs/taxiql/functions#declaring-external-functions) for your functions.

### TaxiFunction
Functions must be annotated with `@TaxiFunction` (don't forget to `import com.orbitalhq.functions.TaxiFunction`)

The following parameters are available on the annotation

| Parameter     | Type   | Meaning                                                                                                                                                                                                                | Default                                                                                             |
|---------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `name`        | String | Optional explicit name of the function in Taxi.                                                                                                                                                                        | Defaults to the Kotlin/Java method name if not provided                                             |
| `description` | String | Optional description - You can use Markdown within the string. Is converted into docs which are exposed via Orbital's data catalog                                                                                     | -                                                                                                   |
| `returnType`  | String | Allows customizing the return type. Provide the fully qualified name of the **Taxi** type this function returns.    If your function returns either a `TypedInstance`, or a non-scalar type, this must be defined | The Taxi Primitive type that corresponds to the declared Kotlin / Java return type of your function |

## Input binding
Custom functions can access the declared parameters, along with various parts of the Orbital execution context,
simply by declaring them in the function signature.

**Note: TaxiParam is all you need (probably)**
Under most scenarios, `@TaxiParam` is all you need. Other parameter types are documented here for completeness, but are only needed in advanced situations

### Parameters
Your function can accept as many input parameters as required. The TaxiQL engine will provide these as passed by callers.
Marshalling to/from Taxi instances [is handled by Orbital](#type-marshalling), allowing you to work directly with the native JVM types.

Parameters must be annotation with `@TaxiParam`, which accepts the following parameters:

| Parameter  | Type                                                                   | Default                                                                                                                     |
|------------|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| `name`     | The name of the parameter as published in the generated Taxi function. | Defaults to the name of the method parameter if avaialble, otherwise `p0`, `p1`, etc.                                       |
| `type`     | The fully qualified Taxi Type name.                                    | Defaults to the corresponding Taxi primitive of the JVM type declared                                                       |
| `nullable` | Indicates if it is acceptable for callers to pass null                 | If not provided, and using Kotlin, then will match the nullability of the Kotlin parameter. Otherwise, defaults to `false`. | 

For example:


```java  Java
import com.orbitalhq.functions.TaxiParam

@TaxiFunction
String sayHello(
   @TaxiParam String name,
   // suffix will be defined of type con.foo.NameSuffix
   // and will be marked as nullable
   @TaxiParam(type = "com.foo.NameSuffix", nullable = true) String suffix
 ) {
    return "Hello " + name
 }
```

```kotlin  Kotlin
import com.orbitalhq.functions.TaxiParam

@TaxiFunction
fun sayHello(
     @TaxiParam name: String,
     // suffix will be defined of type con.foo.NameSuffix
     // and will be marked as nullable, to match the Kotlin definition
     @TaxiParam(type = "com.foo.NameSuffix") suffix: String?
  ):String {
     return "Hello, $name"
  }

```


### Schema
Callers can request the Schema be injected, by declaring an input parameter of type `Schema` (`com.orbitalhq.schemas.Schema`). 

This provides access to the full compiled Taxi schema, which allows for introspection, and is required when manually constructing TypedInstances (though, you normally don't need to do this.)


```java  Java
import com.orbitalhq.functions.TaxiParam
import com.orbitalhq.schemas.Schema

@TaxiFunction
String sayHello(
    @TaxiParam String name,
    Schema schema,
) {
 // ... 
}
```

```kotlin  Kotlin
import com.orbitalhq.functions.TaxiParam
import com.orbitalhq.schemas.Schema

@TaxiFunction
fun sayHello(
   @TaxiParam name: String,
   schema: Schema
):String {
  // ...
}
```


### EvaluationValueSupplier
The EvaluationValueSupplier provides access to the current context under which functions are executing, and provides ability to do things like:

 - Look up values in scope by type
 - Look up values in scope by their declared name
 - Evaluate functions or Taxi queries (including queries that load other data)
 - Construct Taxi objects

This is a powerful - but advanced - supplier. It's unlikely to be needed for most use-cases


```java  Java
import com.orbitalhq.functions.TaxiParam
import com.orbitalhq.models.EvaluationValueSupplier

@TaxiFunction
String sayHello(
    @TaxiParam String name,
    EvaluationValueSupplier valueSupplier,
) {
 // ... 
}
```

```kotlin  Kotlin
import com.orbitalhq.functions.TaxiParam
import com.orbitalhq.models.EvaluationValueSupplier

@TaxiFunction
fun sayHello(
   @TaxiParam name: String,
   valueSupplier: EvaluationValueSupplier
):String {
  // ...
}
```


## Returning values
In it's simplest form, you can just return a Scalar type from your function, and Orbital will handle everything
for you. 

Values can be returning in the following ways:

* Plain values (e.g. `String`, `Int`)
* `TypedInstance` (if you want full control of wrapping values)
* `Result` (Kotlin) — success or failure
* `Either` (Vavr, for Java) — right = success, left = failure
* `null` → converted into a `TypedNull`

JVM scalar types are marshalled to/from Taxi [TypedInstances](#typed-instance) (the internal way that values are stored at runtime by the Taxi engine)
as described in [Type Marshalling](#type-marhsalling)


```java  Java
   import com.orbitalhq.functions.TaxiParam
   import com.orbitalhq.models.EvaluationValueSupplier
   
     @TaxiFunction
>     // Orbital will automatically handle conversion between the JVM String, and a Taxi TypedInstance
>     String sayHello(
       @TaxiParam String name,
   ) {
    // ... 
   }
```

```kotlin  Kotlin
   import com.orbitalhq.functions.TaxiParam
   import com.orbitalhq.models.EvaluationValueSupplier
   
   @TaxiFunction
   fun sayHello(
      @TaxiParam name: String,
      valueSupplier: EvaluationValueSupplier
>    // Orbital will automatically handle conversion between the JVM String, and a Taxi TypedInstance
>    ):String {
     // ...
   }
```


### Returning failures
Sometimes functions can fail, and you want to indicate that to your callers.

Custom functions can model failures using special return types - `Either<>` (Java) or `Result<>` (Kotlin)

**Warning: Avoid throwing exceptions**
Throwing exceptions on the JVM comes with a performance penalty. Within Orbital's function runtime, all exceptions are converted to special nulls

To indicate that a function may fail, Orbital supports functions returning a special return type:

| Language | Return type                                                             |
|----------|-------------------------------------------------------------------------|
| Java     | [Either](https://docs.vavr.io/#_either) from [vavr](https://vavr.io/)   |
| Kotlin   | [Result](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-result/) |

When a function returns a failure, the result is mapped to a `TypedNull` - a special kind of `TypedInstance`, which carries details of what the failure was.


```java Java
 public class SafeDivideFunctions implements TaxiFunctionProvider {
 
    @TaxiFunction(description = "Safely divide two integers. Returns Right(result) or Left(error message).")
    public Either<String, Integer> safeDivide(
          @TaxiParam(name = "numerator") int numerator,
          @TaxiParam(name = "denominator") int denominator) {
 
       if (denominator == 0) {
          // Left = failure
          return Either.left("Cannot divide by zero");
       } else {
          // Right = success
          return Either.right(numerator / denominator);
       }
    }
 }
```

```kotlin Kotlin
class SafeDivideFunctions : TaxiFunctionProvider {

   @TaxiFunction(description = "Safely divide two integers. Returns Result.success or Result.failure.")
   fun safeDivide(
      @TaxiParam(name = "numerator") numerator: Int,
      @TaxiParam(name = "denominator") denominator: Int
   ): Result<Int> {
      return if (denominator == 0) {
         Result.failure(IllegalArgumentException("Cannot divide by zero"))
      } else {
         Result.success(numerator / denominator)
      }
   }
}
```
  

## Providing functions via a JAR file
You can package your functions as a JAR file - authored in either Kotlin or Java - which is copied to your Taxi project.

### Dependencies
Add the following Maven dependencies:

```xml
<dependencies>
  <dependency>
     <groupId>com.orbitalhq</groupId>
     <artifactId>functions-api</artifactId>
     <version>${orbital.version}</version>
  </dependency>
</dependencies>
```

You'll also need to declare the Orbital maven repository:

```xml
  <repositories>
    <repository>
      <id>orbital-releases</id>
      <url>https://repo.orbitalhq.com/release</url>
    </repository>
    {/* optional - for snapshots */}
    <repository>
      <id>orbital-snapshots</id>
      <url>https://repo.orbitalhq.com/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>
```

### Writing functions
Create a class that implements `TaxiFunctionProvider`, and annotate methods with `@TaxiFunction`.  
Function parameters must be annotated with `@TaxiParam`.

```kotlin
package com.acme.functions

class GreetingFunctions : TaxiFunctionProvider {
   @TaxiFunction(description = "Greets a user by name")
   fun greet(@TaxiParam(name = "name") name: String): String =
      "Hello, $name"
}
````

The function can then be used in a query:

```taxi
find { 
  greeting : String = greet("Jimmy") 
}

// returns:
{
   greeting : "Hello, Jimmy"
}
```

### Service Loader
Orbital uses a Java Service Loader to discover the actual functions at runtime, which must be configured.

#### Manually
To manually declare the services, create a file at `src/main/resources/META-INF/services/com.orbitalhq.functions.TaxiFunctionProvider`, with a list of the 
names of your provider classes:

```text META-INF/services/com.orbitalhq.functions.TaxiFunctionProvider
com.acme.functions.GreetingFunctions
```

#### Using Google's Auto Service

Alternatively, you can use Google's [Auto Service](https://github.com/google/auto/tree/main/service) to handle the boilerplate for you.

Add the following Maven dependencies:

```xml pom.xml
<dependency>
   <groupId>com.google.auto.service</groupId>
   <artifactId>auto-service-annotations</artifactId>
   <version>1.1.1</version>
   <scope>provided</scope>
</dependency>
```

And wire it into your maven build (note the different configurations, depending on if your code is in Kotlin or Java)


```xml Kotlin
<plugins>
  <plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <executions>
      <execution>
        <id>kapt</id>
        <goals>
          <goal>kapt</goal>
        </goals>
        <configuration>
          <annotationProcessorPaths>
            <annotationProcessorPath>
              <groupId>com.google.auto.service</groupId>
              <artifactId>auto-service</artifactId>
              <version>1.1.1</version>
            </annotationProcessorPath>
          </annotationProcessorPaths>
        </configuration>
      </execution>
    </executions>
  </plugin>
</plugins>
```
  
```xml Java
<plugins>
  <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <annotationProcessorPaths>
        <path>
          <groupId>com.google.auto.service</groupId>
          <artifactId>auto-service</artifactId>
          <version>${auto-service.version}</version>
        </path>
      </annotationProcessorPaths>
    </configuration>
  </plugin>
</plugins>
```


Then, in your function class, add the `@AutoService` annotation:


```kotlin Kotlin
import com.google.auto.service.AutoService;

@AutoService(TaxiFunctionProvider.class)
class ExampleFunctions : TaxiFunctionProvider {
  // ...snip...  
}
```

```java Java
import com.google.auto.service.AutoService

@AutoService(TaxiFunctionProvider::class)
public class ExampleFunctions implements TaxiFunctionProvider {
  // ...snip...  
}
```


When Orbital loads your project, it will discover the JAR, register your functions into the schema, and make them available to call from Taxi.

### Taxi project configuration
The JAR files must be added to your Taxi project, and the location defined in your `taxi.conf`'s `additionalSources` section, using the key `@orbital/function-jar`

```hocon taxi.conf
   name: orbital/function-jar-example
   version: 0.3.0
   sourceRoot: src/
   additionalSources: {
>      "@orbital/function-jar" : "functions/*.jar"
   }
```

## Writing functions in Kotlin scripts
Kotlin Scripting provides a lightweight alternative to compiling functions into JAR's. Scripts
authored in plain Kotlin, and avoids the build steps required with JARs.
  
* ✅ Fast iteration, no compilation step
* ✅ Easier for quick prototyping
* ✅ Avoids having to deal with ServiceLoader
* ✅ Avoids having to deal with ServiceLoader
* ✅ [Testable](#testing) via Preflight
* ❌ No IDE debugger support
* ❌ Limited CI/CD support  outside of Preflight
* ❌ Unable to use 3rd party libraries
* ❌ Compilation happens at runtime

Kotlin scripting functions are compiled at runtime within Orbital, whenever the schema changes.
However, there's minimal performance difference in terms of actual function execution.

### Writing a kotlin script
For quick iteration, you can define functions inline in `.taxi.kts` scripts.
Scripts are compiled dynamically at runtime.

```kotlin CustomFunctions.taxi.kts
import com.orbitalhq.functions.*

@TaxiFunction
fun greet(@TaxiParam name: String): String = "Hello, $name"
```

When authoring a Kotlin Taxi script, you do not declare a class - simply write the functions directly in the script file.

The `@TaxiFunction`, `@TaxiParam` annotations are automatically added to the classpath at runtime.  

Scripts must use the `.taxi.kts` extension.
Orbital compiles them into providers automatically.

  ### Taxi config for scripts
Kotlin script files should be added to your Taxi project, and the location defined in your `taxi.conf`'s `additionalSources` section, using the key `@orbital/function-kts`

```hocon taxi.conf
   name: orbital/function-jar-example
   version: 0.3.0
   sourceRoot: src/
   additionalSources: {
>      "@orbital/function-kts" : "functions/*.taxi.kts"
   }
```

### Enabling completion support in IntelliJ settings
IntelliJ provides completion support for Kotlin scripting, including Taxi Kotlin scripts.

First, fetch the DSL JAR from the Orbital repository:

If working against a released version:
```bash
mvn dependency:get -Dartifact=com.orbitalhq:functions-kotlin-scripting-api:0.36.0 -DremoteRepositories=https://repo.orbitalhq.com/release
```

If working against a snapshot version:
```bash
mvn dependency:get -Dartifact=com.orbitalhq:functions-kotlin-scripting-api:0.36.0-SNAPSHOT -DremoteRepositories=https://repo.orbitalhq.com/snapshot
```

Then, inside IntelliJ:

* Go to Preferences -> Build, Execution, Deployment -> Compiler -> Kotlin Compiler
* At the bottom you will find a section Kotlin Scripting
* Complete the field: `Script definition template classes to load explicitly`: `com.orbitalhq.functions.kotlin.KotlinTaxiFunctionScript`
* Complete the field: `Classpath required for loading script definition template classes`:  `/.m2/repository/com/orbitalhq/functions-kotlin-scripting-api/0.36.0/functions-kotlin-scripting-api.jar`

Then:
* Go to Preferences -> Language & Frameworks -> Kotlin -> Kotlin Scripting
* Make sure the script template Taxi Custom Functions is active and above the default Kotlin Script
* Apply changes

## Choosing between JARs and Scripts
There are a number of consdierations when choosing between JARs and Kotlin Scripts - though the choice ultimately depends on the preference of the team. 

| Feature                         | JARs                                                           | Kotlin scripts                                                                             |
|---------------------------------|----------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| Debuggable                      | ✅ Via IDE when authoring (not available at runtime in Orbital) | ❌ IDE's have limited support for debugging Kotlin Scripts, no runtime debugging in Orbital |
| Unit-testable                   | ✅ Standard JVM testing approaches                              | ⚠️ (Preflight only)                                                                        |
| Compilation                     | At build time, as part of CI/CD                                | At runtime, on the Orbital serve                                                           |
| Support for 3rd party libraries | ✅                                                              | ❌                                                                                          |
| Loading                         | Requires configuring ServiceLoader                             | Just write the script, Orbital handles everything else                                     |
| Packaging                       | Requires seperate build and copy step - not handled by Orbital tooling | Not required - script file is deployed as-is                                     |

## Appendix

### Namespaces

Always place your custom functions inside a **namespace** to avoid collisions:

```taxi
namespace com.acme.functions

declare function greet(name: String): String
```

Orbital infers the namespace from the package name (for JARs) or script path (for `.taxi.kts`).
Explicit namespaces keep your functions organized and predictable.

### Testing
Custom functions are available for use in [Preflight tests](/docs/testing/unit-testing), so can be used as part of your project's testing strategy.

Additionally, if you're bundling your functions as a JAR file, you can make full use of CI/CD and standard JVM test frameworks  

### TypedInstance
`TypedInstance` is the main way that a Taxi value is held in memory in the JVM.

It's a combination of the following:
 - The actual value
 - The Taxi type
 - It's DataSource (which powers Lineage)

Inputs and Outputs of Functions are always a TypedInstance. However, Orbital will
handle marshalling between the Taxi TypedInstance and the JVM value for you.

You generally don't need to interact with a TypedInstance, unless you're trying to do some advanced things

### Type Marshalling
Values are automatically marshalled between their Taxi and JVM counterparts

| JVM Type                          | Taxi Type      |
|-----------------------------------|----------------|
| `java.lang.Boolean`               | `Boolean`      |
| `java.lang.String`                | `String`       |
| `java.lang.Character`             | `String`       |
| `java.lang.Integer`               | `Integer`      |
| `kotlin.Int`                      | `Integer`      |
| `kotlin.Short`                    | `Integer`      |
| `java.lang.Short`                 | `Integer`      |
| `java.math.BigInteger`            | `Long`         |
| `kotlin.Long`                     | `Long`         |
| `java.lang.Long`                  | `Long`         |
| `java.math.BigDecimal`            | `Decimal`      |
| `kotlin.Double`                   | `Decimal`      |
| `kotlin.Float`                    | `Decimal`      |
| `java.lang.Double`                | `Decimal`      |
| `java.lang.Float`                 | `Decimal`      |
| `kotlin.Double`                   | `Double`       |
| `kotlin.Float`                    | `Double`       |
| `java.lang.Double`                | `Double`       |
| `java.lang.Float`                 | `Double`       |
| `java.lang.Object`                | `Any`          |
| `java.time.LocalDate`             | `LocalDate`    |
| `kotlinx.datetime.LocalDate`      | `LocalDate`    |
| `java.time.LocalTime`             | `Time`         |
| `kotlinx.datetime.LocalTime`      | `Time`         |
| `java.time.LocalDateTime`         | `DateTime`     |
| `kotlinx.datetime.LocalDateTime`  | `DateTime`     |
| `java.time.Instant`               | `Instant`      |
| `kotlinx.datetime.Instant`        | `Instant`      |

## Best practices and considerations

### Consider if you can use an existing function instead
Taxi has a really rich [StdLib](https://taxilang.org/docs/language/stdlib) of common functions, and strong support for [composing existing functions](https://taxilang.org/docs/taxiql/functions#composed-functions)
together to form new functions.

You really only need to declare a custom function if you require capabilities not currently exposed via the StdLib,
or if you wish to encapsulate complex business logic.

### Performance
Unlike most things in Orbital, function evaluation is blocking - it blocks the current thread that the function evaluation is occurring on.

Therefore, you **must** make sure you keep functions fast. While it is technically possible to execute sub-queries within a function,
you should avoid doing so, as it risks slowing down the entire Orbital server while waiting for a response.

However, fetching of arguments to pass into a function is non-blocking. Therefore, rather than running a query to fetch data from 
within your function, define an argument for the data you need as an input to your function.

### Avoid throwing an exception
Throwing exceptions has a performance impact on the JVM, and should be avoided.

Orbital catches all exceptions thrown from a custom function, and marshals it back to a `TypedNull`, with data source of `FailedEvaluatedExpression`,
which contains details of the failure.

Rather than throwing an exception, return one of the following types to signal failure:

 - A [`Result<>`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-result/) (on Kotlin) (via `Result.failure<>(MyException("Boom"))` )
 - A [vavr Either](https://github.com/vavr-io/vavr) (on Java) via `Either.left()`
