All of the available Lua functions are listed below. Additionally, you may access most of the functions available in the Lua 5.2 standard library. If you have questions about the available functions or would like us to make new features available, please let us know in the FarmBot Forum.



Performs an HTTP request to the FarmBot API, returning nil if an error occurs (see logs for details).

This is a limited convenience function that provides an alternative to the http() helper. It has a few differences from http():

  • You do not need to run json.encode on inputs
  • You do not need to run json.decode on outputs
  • The only supported request format is JSON
  • You do not need to pass an auth_token() in the header
  • The base URL is not configurable. is the only endpoint supported.
  • Returns nil if there was an error
  • Errors are sent to the log stream (if any).
result = api({
    method = "post",
    -- Don't forget the leading "/":
    url = "/api/points",
    -- `body` is optional for GET requests.
    body = {
        x = 200,
        y = 200,
        z = 0,
        radius = 100,
        pointer_type = "GenericPointer"

if result then
    send_message("debug", "Point creation OK", "toast")
    send_message("error", "ERROR - See logs for details", "toast")


Returns the device’s authorization token (string). This value can be used to access API resources without the need to store account passwords in Lua code or ENV vars.

-- Fetch all points from API:

resp_json, err = http({
    method = "GET",
    url = "",
    headers = {
        Authorization = ("bearer " .. auth_token()),
        Accept = "application/json"

points = json.decode(resp_json)


Performs an HTTP request. Example:

response, error = http({
  -- OPTIONAL. Default value is "get".
  -- OPTIONAL. Only strings and numbers.
    Authorization="bearer eyJ....4cw",
  -- OPTIONAL. Must be a string. Use included JSON library for
  --           JSON APIs

if error then
  -- The `error` object is reserved for non-HTTP errors.
  -- Example: missing URL.
  -- `error` will be `nil` if no issues are found.
  send_message("info", "Unknown error: " .. inspect(error))
  -- The `response` object has three properties:
  --   `status`: Number              - Response code. Example: 200.
  --   `body`: String                - Response body. Always a string.
  --   `headers`: Map<String, String> - Response headers.


Converts a JSON encoded string to a Lua table:

result, error = json.decode('{"foo":"bar","example":123}')
-- { foo="bar", example=123 }


Converts a Lua variable into stringified JSON.

result, error = json.encode({ foo="bar", example=123 })
-- => '{"foo":"bar","example":123}'


Alias for json.encode.

send_message(type, message, channels)

send_message(type, message, channels?)

The first required parameter is a log type, which is one of the following string values: "assertion", "busy", "debug", "error", "fun", "info", "success", "warn"

The second required parameter is the message, which may be either a string or a number.

The third parameter is optional. It can be a single string or an array of strings. The strings must be one of the following: "ticker", "toast", "email", "espeak"

-- Send a message to the default channel ("ticker"):
send_message("error", "Hello")

-- Send a message to a single channel:
send_message("success", "You've got mail!", "email")

-- Send a message to multiple channels:
send_message("info", "All systems running.", {"toast", "espeak"})


env(key, value) / env(key)

Store and retrieve key/value pairs to disk. This information will be stored on the device SD card and eventually synced with your web app account.

key and value must be strings. No other values are allowed. Values may not exceed 1,000 characters in length.

To create or update a key/value pair:

env("key", "value")

-- Example:
env("MY_API_TOKEN", "abc123")

To retrieve a previously stored value:

-- Returns `nil` if no value is found.

-- Example:
api_token = env("MY_API_TOKEN")
if api_token then
  send_message("info", "Token value is " .. api_token)
  send_message("info", "Value not found")


read_status(...path?) reads the entire device state tree into memory.

The device state tree contains numerous properties that are relevant to the device’s operation. It is the same state tree seen in FarmBotJS.

-- Provide a path to the property you are interested in:
read_status("location_data", "raw_encoders", "x")

-- Alternative syntax:
status = read_status().location_data.raw_encoders.x


Returns a string representation of FarmBot OS’s version.



Returns a string representation of the firmware version on the Farmduino/Arduino.



Fetch device properties. This is the same device resource found on the API.

-- Every property:

-- Single property:


Fetch FarmBot OS configuration properties. This is the same FarmBot OS configuration resource found on the API.

-- Fetch all properties:

-- Fetch single property:


Fetch firmware configuration properties. This is the same firmware configuration resource found on the API.



Update device properties. This is the same device resource found on the API.

update_device({key = "value"})
update_device({name = "Test Farmbot"})


Update FarmBot OS configuration properties. This is the same FarmBot OS configuration resource found on the API.

update_fbos_config({key = "value"})
update_fbos_config({disable_factory_reset = true})


Update firmware configuration properties. This is the same firmware configuration resource found on the API.

update_firmware_config({key = "value"})
update_firmware_config({encoder_enabled_z = 1.0})


coordinate(x, y, z)

Generate a coordinate for use in location-based functions such as move_absolute and check_position.

coordinate(1.0, 20, 30)
-- Returns:
-- {x = 1.0, y = 20,  z = 30}

check_position(coord, tolerance)

check_position(coordinate, tolerance) returns true if the device is within the tolerance range of coordinate.

if check_position({x = 0, y = 0,  z = 0}, 1.23) then
  send_message("info", "FarmBot is at the home position")
home = coordinate(0, 0, 0)
if check_position(home, 0.5) then
  send_message("info", "FarmBot is at the home position")


Returns a table with an x and y attribute represent the maximum length of the garden bed as determined by firmware config settings.

size = garden_size()
send_message("info", "Width: " .. size.y)
send_message("info", "Length: " .. size.x)

get_seed_tray_cell(tray, cell)

Calculates the coordinates of a seed tray cell, such as B3, based on the cell label and the coordinates of the center of the seed tray. See the Pick from Seed Tray featured sequence for an example.

tray = variable("Seed Tray")
cell_label = variable("Seed Tray Cell")
cell = get_seed_tray_cell(tray, cell_label)
cell_depth = 5

-- Send message with cell info
local cell_coordinates = " (" .. cell.x .. ", " .. cell.y .. ", " .. cell.z - cell_depth .. ")"
send_message("info", "Picking up seed from cell " .. cell_label .. cell_coordinates, "toast")

-- Safe Z move to above the cell
job("Moving to Seed Tray", 25)
    x = cell.x,
    y = cell.y,
    z = cell.z + 25,
    safe_z = true


Given a group ID, returns a table of current group member IDs, sorted by the group’s SORT BY method.

group_members = group(1234)
for i,member in ipairs(group_members) do
    plant = api({
        method = "get",
        url = "/api/points/" .. member
    move_absolute(plant.x, plant.y, 0)

Find a group’s ID by navigating to the group in the web app and copying the number at the end of the URL.

soil_height(x, y)

Given an X and Y coordinate, returns a best-effort estimate of the Z axis height of the soil. This function requires at least 3 soil height measurements. When there are less than 3 measurements available, it will return the SOIL HEIGHT setting from the device settings page.

x = 10
y = 29
my_soil_height = soil_height(x, y)
send_message("info", "Distance to soil at (10, 29): " .. inspect(my_soil_height))
-- => "Distance to soil at (10, 29): -409.84"

sort(points, method)

Sorts the given table of points using the chosen sorting method.

points = group(1234)
sorted_points = sort(points, "xy_alternating")
send_message("info", "Second point ID is: " .. sorted_points[2])

The following sorting methods are available. See point group sorting for additional details.

  • xy_ascending
  • yx_ascending
  • xy_descending
  • yx_descending
  • xy_alternating
  • yx_alternating
  • nn (Nearest Neighbor)
  • random

E-stop and Unlock


Emergency locks the Farmduino microcontroller, preventing motor and peripheral usage. Some features, such as send_message, are still available while emergency locked.

-- Lock the device:


Unlock a previously locked device.

-- Unlock the device:


This is an advanced feature that is intended to be used in conjunction with watch_pin.

When called, soft_stop will cancel all current and pending movement requests. Unlike emergency_lock, it will not lock the device nor will it reset the state of peripherals. Commands (including movement commands) will continue normally after a soft stop occurs. This function can be used to pause FarmBot temporarily if a peripheral value changes mid-movement.



Known bug

take_photo returns errors asynchronously, which may lead developers to believe the operation has succeeded when it actually fails in the background. If you require a high level of control over errors or are taking photos beyond the limits that the Web App allows, see take_photo_raw().

Takes a photo using the device camera and uploads it to the web app. Returns nil on success. Returns an error object if capture fails.

error = take_photo()

if error then
  send_message("error", "Capture failed " .. inspect(error))
  send_message("info", "Capture OK")


Every FarmBot has a different garden size and camera viewport. The photo_grid() helper provides developers with a metadata object about the unique camera setup for the current device. The helper is most useful for operations that perform full-garden photography, such as taking a scan of the garden.

Calling photo_grid() will return a table with the following properties:

Property Description
each An iterator function that is called once per cell (see example below).
total The number of cells contained in the photo grid for the device.
x_grid_points The length of the garden scan on the X axis, measured in cells.
y_grid_points The length of the garden scan on the Y axis, measured in cells.
x_grid_start_mm The X coordinate for the center of the first cell in the grid.
y_grid_start_mm The Y coordinate for the center of the first cell in the grid.
x_offset_mm The camera’s relative X offset from the FarmBot position.
y_offset_mm The camera’s relative Y offset from the FarmBot position.
x_spacing_mm The number of millimeters between cells on the X axis.
y_spacing_mm The number of millimeters between cells on the Y axis.
z The height at which the camera was calibrated.

Example: Perform a full-garden photo scan:

local grid = photo_grid()

    if read_status("informational_settings", "locked") then
        move_absolute({x = cell.x, y = cell.y, z = cell.z})
        local msg = "Taking photo " .. cell.count .. " of " ..
        send_message("info", msg)


take_photo_raw() takes a photo using the device camera and holds it in memory. This functionality is useful when uploading photos to 3rd party APIs. If your usecase requires taking hundreds or thousands of photos per-use, you can use take_photo_raw() to upload your images to a third-party image hosting provider that does not impose the same image hosting limits as the Web App.

take_photo_raw() does not upload images to the web app. You must manually upload the resulting images to a hosting provider.

data = take_photo_raw()
return base64.encode(data)


Performs base64 encoding on an object such as an image. Useful for uploading images to 3rd party APIs. Can also be used in reverse: base64.decode().

data = take_photo_raw()
return base64.encode(data)


Performs camera calibration. Calling this function will reset camera calibration settings.


Use the camera to determine soil depth at the current location. Results will be available as point resources in the API. Performing this action over a wide area in many locations will improve the accuracy of soil height readings taken via soil_height(x, y).


Take a photo of the current location. If any vegetation is detected in the photo, it will be added to the device’s list of weeds.



Creates a new job in the jobs popup. This is useful for tracking long running tasks such a photo grids.

start_time = os.time() * 1000

set_job_progress("Scan the garden", {
  status = "working",
  percent = 12.3,
  time = start_time

-- Another string argument, type, can also be added to the job, though this field is no longer used by the FarmBot web app frontend.


Gets the job by name.

job = get_job_progress("Scan the garden")
send_message("debug", "Job progress: " .. job.percent, "toast")



Return a table containing the current X, Y, Z value of the device.

position, error = get_position()

if error then
  send_message("error", error, "toast")
  message = "Y position is " .. position.y
  send_message("info", message)


Move to the 0 (home) position of a given axis.

-- Single axis:

-- Every axis:

move_absolute(x, y, z, s?) / move_absolute(coord)

Move to an absolute coordinate position.

move_absolute(1.0, 2, 3.4)
-- Alternative syntax:
move_absolute(coordinate(1.0, 20, 30))
-- Enable "Safe Z":
  x = 1.0,
  y = 20,
  z = 30,
  safe_z = true

NOTE: move_absolute can accept an optional fourth argument that sets movement speed as a percentage of max speed.


Determines the length of an axis using stall detection, rotary encoders, or limit switch hardware.

-- Single axis:

-- Every axis in the order Z, Y, X:


Finds the 0 (home) position for an axis using stall detection, rotary encoders, or limit switch hardware.

-- Single axis:

-- Every axis in the order Z, Y, X:



Plot a sensor reading point on the sensors panel. Please note that calling new_sensor_reading() does not perform any readings, it only records a value. See also: read_pin()

position, error = get_position()

i = 0
while (i < 10) do
  i = i + 1
    value=(math.random() * 1024)

read_pin(pin, mode?)

read_pin(pin_num, mode?) reads a pin when given a pin number and read mode ("analog" or "digital"). Defaults to "digital" if no mode is given:

pin23 = read_pin(23) -- Digital is the default mode
pin24 = read_pin(24, "digital")
pin25 = read_pin(25, "analog")

set_pin_io_mode(pin, mode)

Sets the I/O mode of an Arduino pin. It is slightly similar to the pinMode() function in the Arduino IDE.

Valid pin modes: "input", "input_pullup", "output".

result, error = set_pin_io_mode(13, "output")
if error then
  send_message("error", inspect(error))
  send_message("info", inspect(result))


Toggles the state of the given pin between digital 1 and 0. If the pin is initially set to an analog value, it will be rounded to the nearest digital value and then toggled.


watch_pin(pin, callback)

Fork the current Lua process into a second, parallel Lua script that is initialized every 500 milliseconds for the duration of the parent script’s lifetime.

Things to keep in mind:

  • The pin watcher feature exists to support internal functionality of the FarmBot, such as motor load monitoring for vacuum and rotary tools. It is an advanced feature that should only be used as a last resort when more simple solutions cannot be used.
  • The callback is a forked copy of the running Lua script. It is not executed in the same script.
  • The callback does not share memory with the calling script. It is completely isolated from its parent. Even though you can access variables with identical names as the parent script, they are not shared. They are duplicate copies.
  • Because the callback is re-initialized every 500 ms. It is not possible to store state in the callback function. Any variables that are modified will be reset upon the next run.
  • The callback is terminated within 500 ms of the parent’s termination.
watch_pin(13, function(data)
  local pin =
  local val = data.value
  local msg = "Pin " .. pin .. " has a value of " .. val
  send_message("debug", msg, "toast")

-- Wait 3 seconds so that the watcher
-- can run a few (~6) times.

write_pin(pin, mode, value)

Sets a pin to a particular mode and value:

write_pin(13, "analog", 128)


Checks the UTM’s tool verification pin as well as the MOUNTED TOOL field in FarmBot’s state tree to verify if a tool is mounted to the UTM.

if not verify_tool() then -- exits sequence if tool verification failed (no tool)



Pause execution for a certain number of milliseconds. Crashes if the value is three minutes or greater.

-- wait for 1 second:

current_month / current_hour / current_minute / current_second

Returns a number representing the current month, hour, minute, or second.



Returns a list of UART devices.

uart_list = uart.list()

for _number, device_path in ipairs(uart_list) do
  send_message("debug", inspect(device_path), {"toast"})
end, baud)

Open a UART device (typically, via USB) for reading and writing. Please note that the UART devices must be connected to the Raspberry Pi, not the Arduino.

-- device name, baud rate:
my_uart, error ="ttyAMA0", 115200)

if error then
    send_message("error", inspect(error), "toast")

if my_uart then
    -- Wait 60s for data...
    string, error2 =
    if error2 then
        send_message("error", inspect(error2), "toast")
        send_message("info", inspect(string), "toast")

    error3 = my_uart.write("Hello, world!")

    if error3 then
        -- Handle errors etc..


See documentation for


See documentation for


See documentation for



If the sequence executing the Lua command contains a sequence variable, you can access its content by calling the variable(name) function:

-- Assumes you are inside of a function that has a variable:
x_pos = variable("parent").x
send_message("info", x_pos, {"toast"});



Executes a subsequence with variables.

-- Execute a subsequence that has a
-- number variable named "time"
subsequence = variable("Subsequence")
sequence(, {
 time = {
   kind = "numeric",
   args = { number = 1500 } }


Wraps a CeleryScript node into an rpc_request.

command = {
 kind = "wait",
 args = {
   milliseconds = 500


Using CeleryScript from the sequence editor

You can use the VIEW CELERYSCRIPT option in the sequence editor to view the shape of raw CeleryScript nodes for the basic commands. However, you cannot perform a straight copy/paste into a Lua command - you will need to remove quotes from keys (eg: "kind" must be written as kind) and change colons to equal signs (: to =).


Allows you to execute arbitrary CeleryScript nodes.

  kind = "rpc_request",
  args = {
    label = "example",
    priority = 500
  body = {
      kind = "move_absolute",
      args = {
        location = {kind = "coordinate", args = {x = 2, y = 2, z = 2}},
        offset = {kind = "coordinate", args = {x = 0, y = 0, z = 0}},
        speed = 100

gcode(command, params)

Calling the function will send raw G code to the Farmduino. No validations will be applied. The function will block the calling process until a response is received from the firmware.

A Q param will implicitly be added by FBOS. Do not explicitly set the Q value. It will cause FBOS to crash.

-- Send "G00 X1.23 Y4.56 Z7.89" to the Farmduino
gcode("G00", { X = 1.23, Y = 4.56, Z = 7.89 })

What’s next?