🎨✨💻 Stay ahead of the curve with handpicked, high-quality frontend development and design news, picked freshly every single day. No fluff, no filler—just the most relevant insights, inspiring reads, and updates to keep you in the know.
Prefer a weekly digest in your inbox? No problem, we got you covered. Just subscribe here.
Bringing new tools into a workflow is always exciting—curiosity bumps up against the comfort of familiar methods. But when our longtime client, Chumbi Valley, came to us with their Valley Adventures project, we saw the perfect opportunity to experiment with Rive and craft cartoon-style animations that matched the playful spirit of the brand.
Rive is a powerful real-time interactive design tool with built-in support for interactivity through State Machines. In this guide, we’ll walk you through how we integrated a .riv file into a React environment and added mouse-responsive animations.
We’ll also walk through a modernized integration method using Rive’s newer Data Binding feature—our current preferred approach for achieving the same animation with less complexity and greater flexibility.
Animation Concept & File Preparation
Valley Adventures is a gamified Chumbi NFT staking program, where magical creatures called Chumbi inhabit an enchanted world. The visual direction leans heavily into fairytale book illustrations—vibrant colors, playful characters, and a whimsical, cartoon-like aesthetic.
To immediately immerse users in this world, we went with a full-section hero animation on the landing page. We split the animation into two parts:
an idle animation that brings the scene to life;
a cursor-triggered parallax effect, adding depth and interactivity.
Several elements animate simultaneously—background layers like rustling leaves and flickering fireflies, along with foreground characters that react to movement. The result is a dynamic, storybook-like experience that invites users to explore.
The most interesting—and trickiest—part of the integration was tying animations to mouse tracking. Rive provides a built-in way to handle this: by applying constraints with varying strengths to elements within a group that’s linked to Mouse Tracking, which itself responds to the cursor’s position.
However, we encountered a limitation with this approach: the HTML buttons layered above the Rive asset were blocking the hover state, preventing it from triggering the animation beneath.
To work around this, we used a more robust method that gave us finer control and avoided those problems altogether.
Here’s how we approached it:
Create four separate timelines, each with a single keyframe representing an extreme position of the animation group:
Far left
Far right
Top
Bottom
Add two animation layers, each responsible for blending between opposite keyframes:
Layer 1 blends the far-left and far-right timelines
Layer 2 blends the top and bottom timelines
Tie each layer’s blend amount to a numeric input—one for the X axis, one for the Y axis.
By adjusting the values of these inputs based on the cursor’s position, you can control how tightly the animation responds on each axis. This approach gives you a smoother, more customizable parallax effect—and prevents unexpected behavior caused by overlapping UI.
Once the animation is ready, simply export it as a .riv file—and leave the rest of the magic to the devs.
How We Did It: Integrating a Rive File into a React Project
Before we dive further, let’s clarify what a .riv file actually is.
A .riv file is the export format from the Rive editor. It can include:
vector graphics,
timeline animations,
a State Machine with input parameters.
In our case, we’re using a State Machine with two numeric inputs: Axis_X and Axis_Y. These inputs are tied to how we control animation in Rive, using values from the X and Y axes of the cursor’s position.
These inputs drive the movement of different elements—like the swaying leaves, fluttering fireflies, and even subtle character reactions—creating a smooth, interactive experience that responds to the user’s mouse.
Step-by-Step Integration
Step 1: Install the Rive React runtime
Install the official package:
npm install @rive-app/react-canvas
Step 2: Create an Animation Component
Create a component called RiveBackground.tsx to handle loading and rendering the animation.
This setup connects directly to the input values defined inside the State Machine, allowing us to update them dynamically in response to user interaction.
State Machine 1 — the name of your State Machine, exactly as defined in the Rive editor
Axis_X and Axis_Y — numeric inputs that control movement based on cursor position
0 — the initial (default) value for each input
☝️ Important: Make sure your .riv file includes the exact names: Axis_X, Axis_Y, and State Machine 1. These must match what’s defined in the Rive editor — otherwise, the animation won’t respond as expected.
Now you can use the RiveBackground like any other component:
<RiveBackground className="hero-background" />
Step 8: Preload the WASM File
To avoid loading the .wasm file at runtime—which can delay the initial render—you can preload it in App.tsx:
import riveWASMResource from '@rive-app/canvas/rive.wasm';
<link
rel="preload"
href={riveWASMResource}
as="fetch"
crossOrigin="anonymous"
/>
This is especially useful if you’re optimizing for first paint or overall performance.
Simple Parallax: A New Approach with Data Binding
In the first part of this article, we used a classic approach with a State Machine to create the parallax animation in Rive. We built four separate animations (top, bottom, left, right), controlled them using input variables, and blended their states to create smooth motion. This method made sense at the time, especially before Data Binding support was introduced.
But now that Data Binding is available in Rive, achieving the same effect is much simpler—just a few steps. Data binding in Rive is a system that connects editor elements to dynamic data and code via view models, enabling reactive, runtime-driven updates and interactions between design and development.
In this section, we’ll show how to refactor the original Rive file and code using the new approach.
Updating the Rive File
Remove the old setup:
Go to the State Machine.
Delete the input variables: top, bottom, left, right.
Remove the blending states and their associated animations.
Group the parallax layers:
Wrap all the parallax layers into a new group—e.g., ParallaxGroup.
Create binding parameters:
Select ParallaxGroup and add:
pointerX (Number)
pointerY (Number)
Bind coordinates:
In the properties panel, set:
X → pointerX
Y → pointerY
Now the group will move dynamically based on values passed from JavaScript.
The Updated JS Code
Before we dive into the updated JavaScript, let’s quickly define an important concept:
When using Data Binding in Rive, viewModelInstance refers to the runtime object that links your Rive file’s bindable properties (like pointerX or pointerY) to your app’s logic. In the Rive editor, you assign these properties to elements like positions, scales, or rotations. At runtime, your code accesses and updates them through the viewModelInstance—allowing for real-time, declarative control without needing a State Machine.
With that in mind, here’s how the new setup replaces the old input-driven logic:
Data Binding is a major step forward for interactive Rive animations. Effects like parallax can now be set up faster, more reliably, and with cleaner logic. We strongly recommend this approach for new projects.
Final Thoughts
So why did we choose Rive over Lottie for this project?
Interactivity: With Lottie, achieving the same level of interactivity would’ve required building a custom logic layer from scratch. With Rive, we got that behavior baked into the file—plug and play.
Optimization: Rive gives you more control over each asset inside the .riv file, and the output tends to be lighter overall.
Our biggest takeaway? Don’t be afraid to experiment with new tools—especially when they feel like the right fit for your project’s concept. Rive matched the playful, interactive vibe of Valley Adventures perfectly, and we’re excited to keep exploring what it can do.
There’s a problem with those thousands of jobs available for Full Stack Engineers or Developers on LinkedIn, like a unicorn, that person isn’t real. Insert spit-take, read it, then tell me what you think.
There I said it, you might think this is a controversial or unpopular opinion but if you hear me out maybe you’ll agree with me, maybe not, but that’s what makes life worth living. I’ve believed this for a long time now, but it’s time to put a little more thought and time to flesh it out.
As for the term Developer or Engineer, yes technically they have different scopes but they mostly cover the same disciplines and principles, so I’ll use them interchangeably from here on out.
To say something doesn’t exist, I should probably first define what people think it is. So Looking across the internet to
A full-stack developer is a developer or engineer who can build both the front end and the back end of a website. The front end (the parts of a website a user sees and interacts with) and the back end (the behind-the-scenes data storage and processing) require different skill sets. Since full-stack…
Recently, at Sovrn, we had an AI Hackathon where we were encouraged to experiment with anything related to machine learning. The Hackathon yielded some fantastic projects from across the company. Everything from SQL query generators to chatbots that can answer questions about our products and other incredible work. I thought this would be a great opportunity to learn more about Apple’s ML tools and maybe even build something with real business value.
A few of my colleagues and I teamed up to play with CreateML and CoreML to see if we could integrate some ML functionality into our iOS app. We got a model trained and integrated into our app in several hours, which was pretty amazing. But we quickly realized that we had a few problems to solve before we could actually ship this thing.
The model was hefty. It was about 50MB. That’s a lot of space to take up in our app bundle.
We wanted to update the model without releasing a new app version.
We wanted to use the model in the web browser as well.
We didn’t have time to solve all of these problems. But the other day I was exploring the Vapor web framework and the thought hit me, “Why not deploy CoreML models on the server?”
Recently, I came across some great inspiration for 3D animations. There are so many possibilities, but it can be tricky to find the right balance and not overdo it. Anything 3D on a website looks especially impressive when scrolled, as the motion reveals the magic of 3D to our eyes, even though the screen is flat (still) 🙂
This one gave me a lot of inspiration for an on-scroll effect:
Your dose of everyday inspiration — Featuring some of the most popular images on Savee™ this week.⁰
And then this awesome reel by Thomas Monavon, too:
So here’s a small scroll experiment with rotating 3D panels, along with a page transition animation using GSAP:
You’ve surely heard the news that GSAP is now completely free, which means we can now use those great plugins and share the code with you! In this specific example, I used the rewritten SplitText and SmoothScroller.
This is just a proof of concept (especially the page transition).
I really hope you enjoy this and find it inspirational!
Understanding the Fetch API can be challenging, particularly for those new to JavaScript’s unique approach to handling asynchronous operations. Among the many features of modern JavaScript, the Fetch API stands out for its ability to handle network requests elegantly. However, the syntax of chaining .then() methods can seem unusual at first glance. To fully grasp how the Fetch API works, it’s vital to understand three core concepts:
In programming, synchronous code is executed in sequence. Each statement waits for the previous one to finish before executing. JavaScript, being single-threaded, runs code in a linear fashion. However, certain operations, like network requests, file system tasks, or timers, could block this thread, making the user experience unresponsive.
Here’s a simple example of synchronous code:
function doTaskOne() { console.log('Task 1 completed'); }
function doTaskTwo() { console.log('Task 2 completed'); }
Let’s dive a bit deeper into the heart of our WebAssembly integration by exploring the key segments of our Go-based WASM code.
involves preparing and specifying our Go code to be compiled for a WebAssembly runtime.
// go:build wasm // +build wasm
These lines serve as directives to the Go compiler, signaling that the following code is designated for a WebAssembly runtime environment. Specifically:
//go:build wasm: A build constraint ensuring the code is compiled only for WASM targets, adhering to modern syntax.
// +build wasm: An analogous constraint, utilizing older syntax for compatibility with prior Go versions.
In essence, these directives guide the compiler to include this code segment only when compiling for a WebAssembly architecture, ensuring an appropriate setup and function within this specific runtime.
package main
import ( "context" "encoding/json" "syscall/js"
"google.golang.org/protobuf/encoding/protojson"
"github.com/Permify/permify/pkg/development" )
var dev *development.Development
func run() js.Func { // The `run` function returns a new JavaScript function // that wraps the Go function. return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// t will be used to store the unmarshaled JSON data. // The use of an empty interface{} type means it can hold any type of value. var t interface{}
// Unmarshal JSON from JavaScript function argument (args[0]) to Go's data structure (map). // args[0].String() gets the JSON string from the JavaScript argument, // which is then converted to bytes and unmarshaled (parsed) into the map `t`. err := json.Unmarshal([]byte(args[0].String()), &t)
// If an error occurs during unmarshaling (parsing) the JSON, // it returns an array with the error message "invalid JSON" to JavaScript. if err != nil { return js.ValueOf([]interface{}{"invalid JSON"}) }
// Attempt to assert that the parsed JSON (`t`) is a map with string keys. // This step ensures that the unmarshaled JSON is of the expected type (map). input, ok := t.(map[string]interface{})
// If the assertion is false (`ok` is false), // it returns an array with the error message "invalid JSON" to JavaScript. if !ok { return js.ValueOf([]interface{}{"invalid JSON"}) }
// Run the main logic of the application with the parsed input. // It’s assumed that `dev.Run` processes `input` in some way and returns any errors encountered during that process. errors := dev.Run(context.Background(), input)
// If no errors are present (the length of the `errors` slice is 0), // return an empty array to JavaScript to indicate success with no errors. if len(errors) == 0 { return js.ValueOf([]interface{}{}) }
// If there are errors, each error in the `errors` slice is marshaled (converted) to a JSON string. // `vs` is a slice that will store each of these JSON error strings. vs := make([]interface{}, 0, len(errors))
// Iterate through each error in the `errors` slice. for _, r := range errors { // Convert the error `r` to a JSON string and store it in `result`. // If an error occurs during this marshaling, it returns an array with that error message to JavaScript. result, err := json.Marshal(r) if err != nil { return js.ValueOf([]interface{}{err.Error()}) } // Add the JSON error string to the `vs` slice. vs = append(vs, string(result)) }
// Return the `vs` slice (containing all JSON error strings) to JavaScript. return js.ValueOf(vs) }) }
Within the realm of Permify, the run function stands as a cornerstone, executing a crucial bridging operation between JavaScript inputs and Go’s processing capabilities. It orchestrates real-time data interchange in JSON format, safeguarding that Permify’s core functionalities are smoothly and instantaneously accessible via a browser interface.
Digging into run:
JSON Data Interchange: Translating JavaScript inputs into a format utilizable by Go, the function unmarshals JSON, transferring data between JS and Go, assuring that the robust processing capabilities of Go can seamlessly manipulate browser-sourced inputs.
Error Handling: Ensuring clarity and user-awareness, it conducts meticulous error-checking during data parsing and processing, returning relevant error messages back to the JavaScript environment to ensure user-friendly interactions.
Contextual Processing: By employing dev.Run, it processes the parsed input within a certain context, managing application logic while handling potential errors to assure steady data management and user feedback.
Bidirectional Communication: As errors are marshaled back into JSON format and returned to JavaScript, the function ensures a two-way data flow, keeping both environments in synchronized harmony.
Thus, through adeptly managing data, error-handling, and ensuring a fluid two-way communication channel, run serves as an integral bridge, linking JavaScript and Go to ensure the smooth, real-time operation of Permify within a browser interface. This facilitation of interaction not only heightens user experience but also leverages the respective strengths of JavaScript and Go within the Permify environment.
// Continuing from the previously discussed code...
func main() { // Instantiate a channel, 'ch', with no buffer, acting as a synchronization point for the goroutine. ch := make(chan struct{}, 0)
// Create a new instance of 'Container' from the 'development' package and assign it to the global variable 'dev'. dev = development.NewContainer()
// Attach the previously defined 'run' function to the global JavaScript object, // making it callable from the JavaScript environment. js.Global().Set("run", run())
// Utilize a channel receive expression to halt the 'main' goroutine, preventing the program from terminating. <-ch }
ch := make(chan struct{}, 0): A synchronization channel is created to coordinate the activity of goroutines (concurrent threads in Go).
dev = development.NewContainer(): Initializes a new container instance from the development package and assigns it to dev.
js.Global().Set("run", run()): Exposes the Go run function to the global JavaScript context, enabling JavaScript to call Go functions.
<-ch: Halts the main goroutine indefinitely, ensuring that the Go WebAssembly module remains active in the JavaScript environment.
In summary, the code establishes a Go environment running within WebAssembly that exposes specific functionality (run function) to the JavaScript side and keeps itself active and available for function calls from JavaScript.
Before we delve into Permify’s rich functionalities, it’s paramount to elucidate the steps of converting our Go code into a WASM module, priming it for browser execution.
For enthusiasts eager to delve deep into the complete Go codebase, don’t hesitate to browse our GitHub repository: Permify Wasm Code.
Kickstart the transformation of our Go application into a WASM binary with this command:
GOOS=js GOARCH=wasm go build -o permify.wasm main.go
This directive cues the Go compiler to churn out a .wasm binary attuned for JavaScript environments, with main.go as the source. The output, permify.wasm, is a concise rendition of our Go capabilities, primed for web deployment.
In conjunction with the WASM binary, the Go ecosystem offers an indispensable JavaScript piece named wasm_exec.js. It’s pivotal for initializing and facilitating our WASM module within a browser setting. You can typically locate this essential script inside the Go installation, under misc/wasm.
However, to streamline your journey, we’ve hosted wasm_exec.js right here for direct access: wasm_exec.
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
Equipped with these pivotal assets — the WASM binary and its companion JavaScript — the stage is set for its amalgamation into our frontend.
To kick things off, ensure you have a directory structure that clearly separates your WebAssembly-related code from the rest of your application. Based on your given structure, the loadWasm folder seems to be where all the magic happens:
loadWasm/ │ ├── index.tsx // Your main React component that integrates WASM. ├── wasm_exec.js // Provided by Go, bridges the gap between Go's WASM and JS. └── wasmTypes.d.ts // TypeScript type declarations for WebAssembly.
To view the complete structure and delve into the specifics of each file, refer to the Permify Playground on GitHub.
Inside the wasmTypes.d.ts, global type declarations are made which expand upon the Window interface to acknowledge the new methods brought in by Go’s WebAssembly:
WebAssembly Initialization: The asynchronous function loadWasm takes care of the entire process:
async function loadWasm(): Promise<void> { const goWasm = new window.Go(); const result = await WebAssembly.instantiateStreaming( fetch("play.wasm"), goWasm.importObject ); goWasm.run(result.instance); }
Here, new window.Go() initializes the Go WASM environment. WebAssembly.instantiateStreaming fetches the WASM module, compiles it, and creates an instance. Finally, goWasm.run activates the WASM module.
React Component with Loader UI: The LoadWasm component uses the useEffect hook to asynchronously load the WebAssembly when the component mounts:
While loading, SVG rocket is displayed to indicate that initialization is ongoing. This feedback is crucial as users might otherwise be uncertain about what’s transpiring behind the scenes. Once loading completes, children components or content will render.
Given your Go WASM exposes a method named run, you can invoke it as follows:
function Run(shape) { return new Promise((resolve) => { let res = window.run(shape); resolve(res); }); }
This function essentially acts as a bridge, allowing the React frontend to communicate with the Go backend logic encapsulated in the WASM.
To integrate a button that triggers the WebAssembly function when clicked, follow these steps:
Creating the Button Component
First, we’ll create a simple React component with a button:
In the code above, the RunButton component accepts two props:
shape: The shape argument to pass to the WebAssembly run function.
onResult: A callback function that receives the result of the WebAssembly function and can be used to update the state or display the result in the UI.
Integrating the Button in the Main Component
Now, in your main component (or wherever you’d like to place the button), integrate the RunButton:
import React, { useState } from "react"; import RunButton from "./path_to_RunButton_component"; // Replace with the actual path
function App() { const [result, setResult] = useState<any[]>([]);
// Define the shape content const shapeContent = { schema: `|- entity user {}
In this example, App is a component that contains the RunButton. When the button is clicked, the result from the WebAssembly function is displayed in a list below the button.
Throughout this exploration, the integration of WebAssembly with Go was unfolded, illuminating the pathway toward enhanced web development and optimal user interactions within browsers.
The journey involved setting up the Go environment, converting Go code to WebAssembly, and executing it within a web context, ultimately giving life to the interactive platform showcased at play.permify.co.
This platform stands not only as an example but also as a beacon, illustrating the concrete and potent capabilities achievable when intertwining these technological domains.
While experimenting with particle systems, I challenged myself to create particles with tails, similar to snakes moving through space. At first, I didn’t have access to TSL, so I tested basic ideas, like using noise derivatives and calculating previous steps for each particle, but none of them worked as expected.
I spent a long time pondering how to make it work, but all my solutions involved heavy testing with WebGL and GPGPU, which seemed like it would require too much code for a simple proof of concept. That’s when TSL (Three.js Shader Language) came into play. With its Compute Shaders, I was able to compute arrays and feed the results into materials, making it easier to test ideas quickly and efficiently. This allowed me to accomplish the task without much time lost.
Now, let’s dive into the step-by-step process of building the particle system, from setting up the environment to creating the trails and achieving that fluid movement.
Step 1: Set Up the Particle System
First, we’ll define the necessary uniforms that will be used to create and control the particles in the system.
Next, create the variables that will define the parameters of the particle system. The “tails_count” variable determines how many segments each snake will have, while the “particles_count” defines the total number of segments in the scene. The “story_count” variable represents the number of frames used to store the position data for each segment. Increasing this value will increase the distance between segments, as we will store the position history of each one. The “story_snake” variable holds the history of one snake, while “full_story_length” stores the history for all snakes. These variables will be enough to bring the concept to life.
tails_count = 7 // n-1 point tails
particles_count = this.tails_count * 200 // need % tails_count
story_count = 5 // story for 1 position
story_snake = this.tails_count * this.story_count
full_story_length = ( this.particles_count / this.tails_count ) * this.story_snake
Next, we need to create the buffers required for the computational shaders. The most important buffer to focus on is the “positionStoryBuffer,” which will store the position history of all segments. To understand how it works, imagine a train: the head of the train sets the direction, and the cars follow in the same path. By saving the position history of the head, we can use that data to determine the position of each car by referencing its position in the history.
Now, let’s create the particle system with a material. I chose a standard material because it allows us to use an emissiveNode, which will interact with Bloom effects. For each segment, we’ll use a sphere and disable frustum culling to ensure the particles don’t accidentally disappear off the screen.
To initialize the positions of the particles, we’ll use a computational shader to reduce CPU usage and speed up page loading. We randomly generate the particle positions, which form a pseudo-cube shape. To keep the particles always visible on screen, we assign them a lifetime after which they disappear and won’t reappear from their starting positions. The “cycleStep” helps us assign each snake its own random positions, ensuring the tails are generated in the same location as the head. Finally, we send this data to the computation process.
For each frame, we compute the position history for each segment. The key aspect of the “computePositionStory” function is that new positions are recorded only from the head of the snake, and all positions are shifted one step forward using a queue algorithm.
Next, we update the positions of all particles, taking into account the recorded history of their positions. First, we use simplex noise to generate the new positions of the particles, allowing our snakes to move smoothly through space. Each particle also has its own lifetime, during which it moves and eventually resets to its original position. The key part of this function is determining which particle is the head and which is the tail. For the head, we generate a new position based on simplex noise, while for the tail, we use positions from the saved history.
To display the particle positions, we’ll create a simple function called “positionNode.” This function will not only output the positions but also apply a slight magnification effect to the head of the snake.
Now, you should be able to easily create position history buffers for other problem-solving tasks, and with TSL, this process becomes quick and efficient. I believe this project has potential for further development, such as transferring position data to model bones. This could enable the creation of beautiful, flying dragons or similar effects in 3D space. For this, a custom bone structure tailored to the project would be needed.
🎨✨💻 Stay ahead of the curve with handpicked, high-quality frontend development and design news, picked freshly every single day. No fluff, no filler—just the most relevant insights, inspiring reads, and updates to keep you in the know.
Prefer a weekly digest in your inbox? No problem, we got you covered. Just subscribe here.
OpenAI’s backend converting messy unstructured data to structured data via functions
OpenAI’s “Function Calling” might be the most groundbreaking yet under appreciated feature released by any software company… ever.
Functions allow you to turn unstructured data into structured data. This might not sound all that groundbreaking but when you consider that 90% of data processing and data entry jobs worldwide exist for this exact reason, it’s quite a revolutionary feature that went somewhat unnoticed.
Have you ever found yourself begging GPT (3.5 or 4) to spit out the answer you want and absolutely nothing else? No “Sure, here is your…” or any other useless fluff surrounding the core answer. GPT Functions are the solution you’ve been looking for.
How are Functions meant to work?
OpenAI’s docs on function calling are extremely limited. You’ll find yourself digging through their developer forum for examples of how to use them. I dug around the forum for you and have many example coming up.
Here’s one of the only examples you’ll be able to find in their docs:
functions = [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["location"], }, } ]
A function definition is a rigid JSON format that defines a function name, description and parameters. In this case, the function is meant to get the current weather. Obviously GPT isn’t able to call this actual API (since it doesn’t exist) but using this structured response you’d be able to connect the real API hypothetically.
At a high level however, functions provide two layers of inference:
Picking the function itself:
You may notice that functions are passed into the OpenAI API call as an array. The reason you provide a name and description to each function are so GPT can decide which to use based on a given prompt. Providing multiple functions in your API call is like giving GPT a Swiss army knife and asking it to cut a piece of wood in half. It knows that even though it has a pair of pliers, scissors and a knife, it should use the saw!
Function definitions contribute towards your token count. Passing in hundreds of functions would not only take up the majority of your token limit but also result in a drop in response quality. I often don’t even use this feature and only pass in 1 function that I force it to use. It is very nice to have in certain use cases however.
Picking the parameter values based on a prompt:
This is the real magic in my opinion. GPT being able to choose the tool in it’s tool kit is amazing and definitely the focus of their feature announcement but I think this applies to more use cases.
You can imagine a function like handing GPT a form to fill out. It uses its reasoning, the context of the situation and field names/descriptions to decide how it will fill out each field. Designing the form and the additional information you pass in is where you can get creative.
GPT filling out your custom form (function parameters)
One of the most common things I use functions for to extract specific values from a large chunk of text. The sender’s address from an email, a founders name from a blog post, a phone number from a landing page.
I like to imagine I’m searching for a needle in a haystack except the LLM burns the haystack, leaving nothing but the needle(s).
GPT Data Extraction Personified.
Use case: Processing thousands of contest submissions
I built an automation that iterated over thousands of contest submissions. Before storing these in a Google sheet I wanted to extract the email associated with the submission. Heres the function call I used for extracting their email.
{ "name":"update_email", "description":"Updates email based on the content of their submission.", "parameters":{ "type":"object", "properties":{ "email":{ "type":"string", "description":"The email provided in the submission" } }, "required":[ "email" ] } }
Assigning unstructured data a score based on dynamic, natural language criteria is a wonderful use case for functions. You could score comments during sentiment analysis, essays based on a custom grading rubric, a loan application for risk based on key factors. A recent use case I applied scoring to was scoring of sales leads from 0–100 based on their viability.
Use Case: Scoring Sales leads
We had hundreds of prospective leads in a single google sheet a few months ago that we wanted to tackle from most to least important. Each lead contained info like company size, contact name, position, industry etc.
Using the following function we scored each lead from 0–100 based on our needs and then sorted them from best to worst.
{ "name":"update_sales_lead_value_score", "description":"Updates the score of a sales lead and provides a justification", "parameters":{ "type":"object", "properties":{ "sales_lead_value_score":{ "type":"number", "description":"An integer value ranging from 0 to 100 that represents the quality of a sales lead based on these criteria. 100 is a perfect lead, 0 is terrible. Ideal Lead Criteria:\n- Medium sized companies (300-500 employees is the best range)\n- Companies in primary resource heavy industries are best, ex. manufacturing, agriculture, etc. (this is the most important criteria)\n- The higher up the contact position, the better. VP or Executive level is preferred." }, "score_justification":{ "type":"string", "description":"A clear and conscise justification for the score provided based on the custom criteria" } } }, "required":[ "sales_lead_value_score", "score_justification" ] }
Define custom buckets and have GPT thoughtfully consider each piece of data you give it and place it in the correct bucket. This can be used for labelling tasks like selecting the category of youtube videos or for discrete scoring tasks like assigning letter grades to homework assignments.
Use Case: Labelling news articles.
A very common first step in data processing workflows is separating incoming data into different streams. A recent automation I built did exactly this with news articles scraped from the web. I wanted to sort them based on the topic of the article and include a justification for the decision once again. Here’s the function I used:
{ "name":"categorize", "description":"Categorize the input data into user defined buckets.", "parameters":{ "type":"object", "properties":{ "category":{ "type":"string", "enum":[ "US Politics", "Pandemic", "Economy", "Pop culture", "Other" ], "description":"US Politics: Related to US politics or US politicians, Pandemic: Related to the Coronavirus Pandemix, Economy: Related to the economy of a specific country or the world. , Pop culture: Related to pop culture, celebrity media or entertainment., Other: Doesn't fit in any of the defined categories. " }, "justification":{ "type":"string", "description":"A short justification explaining why the input data was categorized into the selected category." } }, "required":[ "category", "justification" ] } }
Often times when processing data, I give GPT many possible options and want it to select the best one based on my needs. I only want the value it selected, no surrounding fluff or additional thoughts. Functions are perfect for this.
Use Case: Finding the “most interesting AI news story” from hacker news
I wrote another medium article here about how I automated my entire Twitter account with GPT. Part of that process involves selecting the most relevant posts from the front pages of hacker news. This post selection step leverages functions!
To summarize the functions portion of the use case, we would scrape the first n pages of hacker news and ask GPT to select the post most relevant to “AI news or tech news”. GPT would return only the headline and the link selected via functions so that I could go on to scrape that website and generate a tweet from it.
I would pass in the user defined query as part of the message and use the following function definition:
{ "name":"find_best_post", "description":"Determine the best post that most closely reflects the query.", "parameters":{ "type":"object", "properties":{ "best_post_title":{ "type":"string", "description":"The title of the post that most closely reflects the query, stated exactly as it appears in the list of titles." } }, "required":[ "best_post_title" ] } }
Filtering is a subset of categorization where you categorize items as either true or false based on a natural language condition. A condition like “is Spanish” will be able to filter out all Spanish comments, articles etc. using a simple function and conditional statement immediately after.
Use Case: Filtering contest submission
The same automation that I mentioned in the “Data Extraction” section used ai-powered-filtering to weed out contest submissions that didn’t meet the deal-breaking criteria. Things like “must use typescript” were absolutely mandatory for the coding contest at hand. We used functions to filter out submissions and trim down the total set being processed by 90%. Here is the function definition we used.
{ "name":"apply_condition", "description":"Used to decide whether the input meets the user provided condition.", "parameters":{ "type":"object", "properties":{ "decision":{ "type":"string", "enum":[ "True", "False" ], "description":"True if the input meets this condition 'Does submission meet the ALL these requirements (uses typescript, uses tailwindcss, functional demo)', False otherwise." } }, "required":[ "decision" ] } }
If you’re curious why I love functions so much or what I’ve built with them you should check out AgentHub!
AgentHub is the Y Combinator-backed startup I co-founded that let’s you automate any repetitive or complex workflow with AI via a simple drag and drop no-code platform.
“Imagine Zapier but AI-first and on crack.” — Me
Automations are built with individual nodes called “Operators” that are linked together to create power AI pipelines. We have a catalogue of AI powered operators that leverage functions under the hood.
Our current AI-powered operators that use functions!
If you want to start building AgentHub is live and ready to use! We’re very active in our discord community and are happy to help you build your automations if needed.