Extending with code

Gadget can be extended in a variety of ways using JavaScript files that interplay with the existing bits of the platform. You can add Validations, Preconditions, and Effects written in JS to make your application do exactly what it needs to.

For Computed fields, refer to the Computed Fields guide.

When to write code

Code is the right tool to solve many problems for developers, and there's a whole lot of it out there already! Gadget is built for this reality. While Gadget handles a good amount of the boilerplate, there's lots of business logic unique to your problem that is best captured in code.

We recommend that when there is a platform tool that solves your problem, you try to use it before dropping down to coding your own. The Gadget team is constantly adding features and fixing bugs so that you don't have to do it yourself. Where possible, staying on the upgrade train is generally preferable so your application stays correct, performant, and secure.

If you need to write code though, you should not hesitate! Gadget strives to be an excellent neighbour and provides all the same runtime tooling for custom code as it does for the built in primitives. Your code can use the Gadget logging system, the scalable runtime, and do all the same things the builtins do like writing data to the database. If code is the right tool for the job, you should go right ahead and use it.

Deploying new code

Gadget is a deployless system -- all code written in the Gadget editor is running live in the cloud as soon as it is saved. This allows you to make changes and then preview your application immediately, keeping your feedback loop short and helping you get stuff done. Your code runs with the full power of Gadget's optimized, elastic cloud systems; so if you need to chew through a lot of data during a big migration, or access sensitive third party APIs, you're able to do so in development just as easily as you might in production.

There's no bundling or build step required when writing code for Gadget's runtime. So, you don't need to assemble your different files into a .zip or giant JS file before you can run your application, or fight with build tools to create exactly the right artifact for deployment 🎉


Gadget has a variety of built in validations that cover the common cases for data validation. If you have a complicated or unique validation rule that governs data in your application, Gadget supports adding your own validations written in JavaScript. The Run Code validation runs a JS function each time a record is created or updated to check if that change should succeed. If the function reports no errors, then the save will complete, and if the data is somehow invalid, the validation can add structured error messages to the different fields of the record being changed.

Each Run Code validation file must export a function as the default export function from the module. The function can be marked async or return a Promise The function doesn't need to return anything, and instead, should add errors to any fields determined to be invalid using the passed in errors object. The errors object is an Errors that holds a structured set of errors keyed by field.

By convention, Run Code validations are placed in a root level folder named after the model, and then in the validations folder. For a model named Widget for example, the Gadget editor will suggest you add the first validation at widget/validations/some-useful-code.js. You can move or rename these files however you wish.

Validation functions get passed one big context object and not individual arguments. This is because there is a lot of arguments you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they need. The most common keys used from the context are the record itself at the record key, the api object which is an already set up instance of the JavaScript client for the application at api, and the Errors object at the errors key.

Validation examples

Here's a validation that tests if a record's full name is not null, its last name is also not null:

module.exports = async ({ record, errors }) => {
if (record.firstName && !record.lastName) {
errors.add("lastName", "must be set if the first name is set");

Here's a validation that tests if a record's phone number is valid:

1const PhoneNumber = require("awesome-phonenumber");
3module.exports = async ({ record, errors }) => {
4 const number = new PhoneNumber(record["customerPhoneNumber"]);
5 if (!number.isValid()) {
6 errors.add("customerPhoneNumber", "is not a valid phone number");
7 }

If a validation function throws an error while executing, the action validating the current record will fail and its effects rolled back.


Preconditions capture logic about when an action can be taken. If the preconditions fail, no action effects run. Preconditions are useful specifically because they can be run independently of the action itself to figure out if the action can currently be taken. This is great for disabling buttons that run actions in a UI or hiding bits of functionality a user is unable to access.

Preconditions are all expressed as files of JavaScript code. Each Precondition file is expected to be a valid JavaScript module that exports a function that returns true or false as its default export. The function can be asynchronous, and will be awaited by the Gadget runtime before running the rest of the preconditions or action. If the precondition returns true, the action execution will proceed, and if it returns false, the action's execution will be halted and any database effects rolled back.

When you create a precondition in the Gadget behavior system, Gadget will automatically create you a .js file to hold your preconditions's source code. It will be placed in a root level folder named after the model, and then in a folder named after the action the precondition is on, and then in the preconditions folder. For an action named Update on a model named Widget for example, you'd find the first precondition you add to Update at widget/update/preconditions/check-A.js. You can rename this file to make the name more descriptive if you like.

Precondition functions get passed one big context object and not individual arguments. This is because there is a lot of arguments you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they do need. The most common keys used from the context are the record itself at the record key and the api object which is an already set up instance of the JavaScript client for the application at api.

If a precondition function throws an error while executing, the action acting on the current record will fail, and its effects will be rolled back.

Precondition examples

Here's a precondition which only allows a record to be updated if the record's owner field matches the current logged in user:

module.exports = ({ api, record, session }) => {
const currentUser = session.get("user");
return record.owner._link == currentUser._link;

Here's a precondition which only allows blog posts to be published on weekdays:

module.exports = () => {
const currentDay = new Date().getDay();
return currentDay > 1 && currentDay < 6;

Here's a precondition which only allows tasks in a todo application to be started if their due date is present. This would allow tasks to be created in an unstarted state without due dates, but prevent them from being started until the due date is set.

module.exports = ({ record }) => {
return record.dueDate != null;


Effects do useful stuff for users of Gadget applications. Gadget has some built in effects for basic manipulation of the database, but often, specific bits of business logic need to be written in code to solve a problem well. Run Code effects are where business logic that changes things in the database or writes to third party APIs should go. Run Code effects are run in one of three separate stages: the Run Effects, the Success Effects, or the Failure Effects, and more information about the transaction boundaries around these effect lists can be found in the Behavior Guide. The interface for effect code is the same regardless of which effect list an effect runs in.

Like Validations and Preconditions, each Run Code effect file must export a function as the default export from the module. If asynchronous, this function will be awaited by the Gadget runtime during action execution. The function should run whatever logic it needs to, which most often is using the api object to make changes to the Gadget database, or inspecting the record object to send its data elsewhere.

Each Run Code effect lives in one .js file. When you create a Run Code effect, Gadget will automatically create a .js file to hold the effect's source code. Once you name the file, it will be placed in a root level folder named after the model, and then in a folder named after the action the effect runs within. For an action named Update on a model named Widget, for example, you'd find the first Run Code effect you add to Update at widget/update/yourFileName.js. Existing files in the widget/update directory will be shown, however if you'd like to use an existing file that is not located in this directory you may start typing to search for the file.

Effect context

Run Code functions get passed one argument, which is a big EffectContext context object full of useful data. They don't get passed individual arguments. This is because there is a lot of different elements you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they do need. The most common keys used from the context are the record itself at the record key, and the api object which is an already set up instance of the JavaScript client for the application at api.

The EffectContext object passed into each effect function has the following keys:

  • api: An connected, authorized instance of the generated API client for the current Gadget application.
  • params: The incoming data from the API call invoking this action
  • record: The root record this action is operating on
  • session: An record representing the current user's session, if there is one
  • config: An object of all the configuration values created in Gadget's Configuration editor
  • connections: An object containing client objects for all connections
  • logger: A logger object suitable for emitting log entries viewable in Gadget's Log Viewer
  • model: An object describing the metadata for the model currently being operated on, like the fields and validations this model applies

record use in Effects

For Model Actions, Gadget automatically loads the record that the action is being taken on from the database at the start of the action. The record is stored in the record key on the action context, so it is there if you'd like to access it. Effects that change the record should apply those changes to the record object on the context. Note that the record is not automatically saved, and that you must use a Create Record or Update Record effect to persist record changes, or use the api object to persist exactly the changes you want from within a Run Code effect.

The record passed to an effect may not be persisted yet. In the case of a Create action for example, there will be a record in the context, but it won't have an id assigned yet. To persist the record and assign it an id, run a Create Record effect first, or use the Internal API via api.internal to manually save the record in your code.

api use in Effects

The api object that gets passed into an effect function is an instance of the generated JavaScript client for your Gadget application. It works the same way in an effect as it does elsewhere, and has all the same functions documented in the API Reference. You can use the api object to fetch other data in an effect:

1// example: send a notification to the user who owns the record if they've opted in
2const twilio = require("../../twilio-client");
3module.exports = async ({ api, record }) => {
4 const creator = await api.users.findOne(record.creator._link);
5 if (creator.wantsNotifications) {
6 await twilio.sendSMS({
7 to: creator.phoneNumber,
8 from: "+11112223333",
9 body: `Notification: ${record.title} was updated`,
10 });
11 }

Or you can use the api object to change other data in your application as a result of the current action:

1// example: save an audit log record when this record changes
2module.exports = async ({ api, record, model }) => {
3 const changes = record.changes();
5 // run a nested `create` action on the `auditLog` model
6 await api.auditLogs.create({
7 action: "Update",
8 model: model.apiIdentifier,
9 record: { _link: record.id },
10 changes: changes.toJSON(),
11 });

Sub-action execution in Gadget is still governed by the same rules as top level action execution: actions need to pass the preconditions and records need to be in the right state for the action being executed.

Using the Public API vs the Internal API

Depending on what you're trying to accomplish with an effect, it is sometimes appropriate to use the Public API of your application, via api.someModel.create, and sometimes it's appropriate to use the Internal API, via api.internal.someModel.create.

Since effects are used to run the important business logic of your application, you will generally want to use the Public API. The Public API for a Gadget application invokes Model Actions and Global Actions with all their preconditions and effects. This means that if you call api.someModel.create() within an effect, you are invoking another action from within the currently invoking action. This is supported by Gadget, and often the right thing to do if your business logic needs to trigger other high level logic.

Sometimes though, you may want to skip executing the effects that make up an action. The Internal API does just this. Internal API endpoints make low level changes to the Gadget database, and don't run effects or preconditions of any action. The Internal API can be used safely within effects by running api.internal.someModel.<action name>. You can think of the Internal API like raw SQL statements in other frameworks where you might run explicit INSERT or UPDATE statements that skip over the other bits of the framework. The Internal API is significantly faster, and is often used for building high volume scripts that need to import data or touch many records quickly. Because the Internal API skips important logic for actions though, it is generally not recommended for use unless you have an explicit reason.

Using the Internal API to re-update records

The Internal API is also useful in situations where you want to make changes to a record after other parts of an action have run. If you are building a Create action that uses the default Apply Params and Create Record effects before a Run Code effect, the record will already be saved to the database by the time the Run Code effect runs. The Update action isn't available at this point as the record is still transitioning into the Created state. Instead, you can use the Internal API (with api.internal.someModel.update) to make more changes to the record in subsequent effects, without worrying what state the record is in. This approach isn't ideal however, as it makes two calls to the Gadget Internal API.

The Run Code effect above might look like:

module.exports = ({ api, record }) => {
await api.internal.someModel.update(record.id, {
someModel: { title: "new title" },

However, this double call to the Internal API makes this action slower and a bit more error prone. Instead, it's fastest and safest to change the record object several times, and then save the changes once to the database as the last step. We do this by reordering the effects to run custom code that modifies the record before the Create Record effect runs.

The Run Code effect changes to modify the record object in memory, and relies on the next effect to save the in memory object to the database.

module.exports = ({ api, record }) => {
record.title = "new title";

Action Infinite Loops

Be careful when invoking actions within effects, especially if invoking the same action from within itself.

Invoking actions within actions can cause infinite loops, so care must be taken to avoid this. If you invoke the currently underway action from within itself, your effects will run again, potentially invoking the action again in a chain. Or, if you invoke an action in model B from model A, but model B invokes an action in model A, you can create a chain of action invocations that never ends. Action chains like this can show up in the logs as timeouts or errors that happen at an arbitrary point in an action.

To avoid infinite loops, you must break the loop somehow. There's a couple options for this:

  • use record.changed('someField') to only run a nested action some of the time when data you care about has changed
  • use the Internal API instead of the Public API (api.internal.someModel.update instead of api.someModel.update) to make changes, skipping the effect stack that retriggers the loop

Connection Action Infinite Loops

Be careful when invoking remote APIs within effects if those remote APIs might fire webhooks that call back to your application, re-invoking the same action.

Calling third party APIs from your effects is a common, but it's important to be aware that third party APIs may trigger subsequent actions in your Gadget app in response to changes.

For example, if you have a Gadget app connected to a Shopify store with a connected Shopify Product model, every time Shopify sends the app a product/updated webhook Gadget will run the Update action. If the business logic inside the Update action makes API calls back to Shopify to update the product again (say to update the tags of a product in response to a change in the other fields), the API call back to Shopify will trigger a second webhook sent back to Gadget. Without care, this can cause an infinite loop of actions triggering webhooks that retrigger the actions.

See Action infinite loops for strategies to break this infinite loop.

connections use in Effects

The connections object in an effect's context contains prebuilt client objects with the appropriate set of credentials, which you can use to make remote API calls. This is useful when the remote calls you need aren't provided as effects, or you need them to happen conditionally.

For example, suppose you have a Shopify connection to your store best-sneakers.myshopify.com and you want to update a product. You could write an effect with this:

const shopifyClient = await connections.forShopDomain("best-sneakers.myshopify.com");
await shopifyClient.product.update(productId, productParams);

It's important to keep in mind that changing your remote data could result in an update webhook being sent back to Gadget. Care will need to be taken to ensure you don't get into an infinite feedback loop with the remote system. One good way to ensure this doesn't happen is to make remote updates in success effects. This guarantees the record has been committed to the database, so you can check the incoming webhook to see if a relevant field has changed before making the remote call.

Visit the Connections guide to learn more about each of the specific clients.

Specifying custom params

Sometimes actions need to accept parameters that aren't directly stored in the model, like a sendNotifications param that might enable or disable some business logic for sending emails. Custom params are added to an action by exporting a params object from a custom code effect on the action. Gadget expects the exported params object to be a subset of a JSON schema specification. For example, if you want to expose a boolean flag and name object in your action or global action:

1module.exports = async ({ params }) => {
2 if (params.sendNotifications) {
3 // ...
4 }
7module.exports.params = {
8 sendNotifications: { type: "boolean" },
9 fullName: {
10 type: "object",
11 properties: {
12 first: { type: "string" },
13 last: { type: "string" },
14 },
15 },

With that in place in your code effect, you can now make the following GraphQL call:

1# action
2mutation {
3 myActionModelA(
4 sendNotifications: true
5 fullName: { first: "Jordan", last: "Stag" }
6 ) {
7 success
8 # …other selections…
9 }
12# global action
13mutation {
14 myAction(sendNotifications: true, fullName: { first: "Jordan", last: "Stag" }) {
15 success
16 # …other selections…
17 }

The params specification must come after the default export, module.exports = ..., so that there's something to attach the params specification to. Also note from the above example that the schema is written as a JavaScript object.

Gadget currently supports the following types from JSON schema:

  • object
  • string
  • integer
  • number
  • boolean
  • array

Only these primitive types are currently supported by Gadget. No other features of JSON schema are currently available, so don't use validations, required and/or schema composition primitives like allOf. If you want to validate parameter values, you can do so in the code for the effect.

Global Action Run Code Effects

Global Actions can also use Run Code effects for whatever they need to accomplish. Run Code effects within Global Actions won't be passed a record object or a model object, but they will still be passed the params, the config, the logger, and all the associated goodies you would find in a model Run Code effect.

Effect examples

Here's an example effect which updates a user's fullName field to be a combination of the firstName field and lastName field:

module.exports = async ({ api, record, session }) => {
await api.internal.user.update(record.id, {
user: { fullName: record.firstName + " " + record.lastName },

Note that this effect uses api.internal.user.update, so it's using the Internal API to make a fast, simple update to the User model in the Gadget database, and is not running the Update action of the User model.

Here's an example effect which checks if an incoming purchase is above a certain amount, and if so, creates a Fraud Review record to power a business process to take a look at the especially high value order:

1const HIGH_VALUE_THRESHOLD = 20000; // cents
3module.exports = async ({ api, record, session }) => {
4 if (record.totalPrice.amount > HIGH_VALUE_THRESHOLD) {
5 await api.fraudReviews.start({ order: { _link: record.id } });
6 }

Note that this effect uses the api.fraudReviews.start function, which triggers the execution of another action, the Start action on the Fraud Review model. This is desirable because perhaps that action itself has some effects that are important, so this code can run those effects without needing to know exactly what they are.

Here's an example effect which uses the Akismet spam detection API to ensure a comment isn't spammy during the Create action:

1const { AkismetClient } = require("akismet-api");
3// instantiate the client for the remote service
4const client = new AkismetClient({
5 key: "my-api-key",
6 blog: "https://myblog.com",
9module.exports = async ({ api, record }) => {
10 const isSpam = await client.checkSpam({
11 content: record.body,
12 email: record.email,
13 });
14 if (isSpam) {
15 // throwing this error will roll back the transaction, aborting any in progress creates or updates
16 throw new Error("Can't save the comment because it seems spammy");
17 }

We model spam detection here as an effect, not as a precondition, because we want to allow anyone to try to create comments, and only know that they are spammy once they have given us the comment they want to create. It'd be great if somehow we knew who spammers were ahead of time, but we need them to attempt to comment before we can make that assessment.

Sharing code between files

All the code for your Gadget application is stored in a directory structure representing a plain old node.js module, so you can do all the things you normally can with a plain old node.js module like requiring files! You can create shared utilities and put them in whatever folder structure you like.

For example, if you have a Gadget application with these files:


and you'd like to share a utility function between those two run effects, you could create a file at widget/utils.js with the function in it:

module.exports.leftPad = (string, padding) => string.padStart(padding);

and then require it in widget/create/run-A.js and widget/update/run-A.js:

widget/create/run-A.js and widget/update/run-A.js
const { leftPad } = require("../utils");
module.exports = async ({ api, scope }) => {
// ... effect code that uses `leftPad`

You can also create other folders or put source code files at the root of your project. We recommend sticking to the one-folder-per-model convention for structuring your validation, precondition, and effect code so that when Gadget generates you the boilerplate it will be close to the other code that touches the same model.

Sharing code effects between models

If you want to share a code file between actions on different models, and need to customize the behaviour of the code file based on the model you're working with, you can do so by discriminating on the __typename property of your record.

In JavaScript code effects, you can use jsdocs to @typedef both context types that use this code effect, and define a union type for the context param.

2 * Effect code for Widgets and Gizmos
3 * @typedef { import("gadget-server").CreateWidgetActionContext } CreateWidgetActionContext
4 * @typedef { import("gadget-server").CreateGizmoActionContext } CreateGizmoActionContext
5 * @param {CreateWidgetActionContext | CreateGizmoActionContext} context
6 */
7module.exports = async ({ record }) => {
8 if (record.__typename == "Widget") {
9 // do something specific to widgets
10 } else if (record.__typename == "Gizmo") {
11 // do something specific to gizmos
12 }

The editor will typecheck your record and the fields available in each block of the if statement will be narrowed to fields on that model. The __typename of each record is the camelized apiIdentifier of its model.

Packages from npm

Gadget supports installing node.js packages from the npm package registry like you might in other node.js systems. Each Gadget application has a standard, spec-compliant package.json file at the root which lists the packages the application needs to run. To add a package to your application, add a new key to the dependencies object inside package.json, and Gadget will install the most recent version of that package matching your version constraint. If Gadget fails to install your dependencies, a small red status icon will appear beside the package.json entry in the file tree with an error message explaining the issue.

Once installed, packages can be required by requiring them like you might in any other node.js project using require.

For example, if we add lodash to a package.json so it looks like this:

2 "name": "an-example-gadget-app",
3 "version": "0.1.0",
4 "private": true,
5 "description": "Internal package for Gadget app an-example-gadget-app (Production environment)",
6 "dependencies": {
7 "@gadget-client/an-example-gadget-app": "link:.gadget/client",
8 "lodash": "^4.17.0"
9 }

then we can require it in a validation, precondition, or effect file with require:

some effect or precondition file
const _ = require("lodash");
module.exports = async ({ api, scope }) => {
// ... effect code that uses `_.defaults` or `_.groupBy` or any other Lodash helper function.

Gadget also requires that your app depend on the generated Gadget API client. All Gadget apps are generated with an existing dependency on the Gadget API client, which the platform places into node_modules/.gadget automatically. This dependency shouldn't be removed.

Environment variables

Environment variables are variables whose values are set outside of the code that's leveraging them. Environment variables are often used to store sensitive information that the developer does not want to expose in code, say API credentials.

Gadget offers built-in support for storing and accessing environment variables.

Managing environment variables

To store environment variables in Gadget, click on Settings>Environment Variables in the navigation menu. To add a new environment variable, click on "+ Add Variable" and complete the form as displayed below.

Managing environment variables in Gadget

If the variable in question is sensitive and you wish to hide its value from being displayed in the Gadget editor, you can simply click on the lock icon next to the value input. You can also edit environment variables by overriding the value in the input field. Finally, environment variables can be deleted, simply by clicking on "Remove".

Using environment variables in code

Gadget allows you to set environment variables for use within code effects and all JavaScript files. These values are accessible using two different methods:

First, your app's environment variables are available within the config property of an action's effect context. Here's an example use case within a Global Action:

2 * Effect code for global action Example
3 * @typedef { import("gadget-server").ExampleGlobalActionContext } ExampleGlobalActionContext
4 * @param {ExampleGlobalActionContext} context - Everything for running this effect, like the api client, current record, params, etc
5 */
6module.exports = async ({ logger, config }) => {
7 logger.info({ value: config.FOO }, "logging an environment value");

Second, to allow access outside of action contexts, such as in shared library code, all environment variables are also available within the process' environment, which you can access with process.env:

* Shared Example client
module.exports = new ExampleClient(process.env["EXAMPLE_AUTH_TOKEN"]);

Runtime environment

Gadget runs JavaScript code, with support for TypeScript and other languages on the way. Gadget runs JavaScript using node.js version 16, and runs code inside containers on Linux. This means Gadget has support for:

  • JS packages from the npm package registry, see the section on Packages from npm
  • JS packages that require native extensions, like sharp or esbuild
  • including other file types in your app and reading them, like metadata files in JSON or YAML
  • including images and other assets you might need

Gadget's backend node.js processes are orchestrated using a fast, custom serverless runtime layer running in Google Cloud Platform. Each Gadget application is automatically deployed when anything changes, and each app is scaled up and down as needed without any manual knob-turning required.

Gadget's JavaScript editor offers full TypeScript type support, meaning that you get type checking, autocomplete, and editor support for JS and TS when you write code in Gadget.

You can disable typechecking on any of your JavaScript files by adding the comment "// @ts-nocheck" to the file. You can disable typechecking on a single line by commenting "// @ts-ignore" instead.

Module system

The node.js runtime powering your Gadget application supports code written in the CommonJS style (with require statements) as well as the ECMAScript Modules style (with import statements) out of the box. Gadget compiles your code down to CommonJS to execute on node, so if you use import syntax or other newer ESM features, they will be transpiled to code that node supports out of the box, as opposed to run as they are.

For example, if you want to use ESM to write effect functions for a Gadget model, you can use import and export:

1import twilio from "../../twilio-client";
3export default async function ({ api, record }) {
4 const creator = await api.users.findOne(record.creator._link);
5 if (creator.wantsNotifications) {
6 await twilio.sendSMS({
7 to: creator.phoneNumber,
8 from: "+11112223333",
9 body: `Notification: ${record.title} was updated`,
10 });
11 }

Gadget uses swc for code transpilation for minimal runtime overhead when executing code that needs to be transpiled.


Every EffectContext will have a logger object suitable for outputting logs viewable via Gadget's Log Viewer. In addition, the Gadget Log Viewer will also display some logs generated by the Gadget platform.

2 * Effect code for a run effect Example
3 * @typedef { import("gadget-server").ExampleRunEffectContext } ExampleRunEffectExample
4 * @param {ExampleRunEffectContext} context - Everything for running this effect, like the api client, current record, params, etc
5 */
7module.exports = ({ api, record, logger }) => {
8 logger.info({ record }, "Updating record");
9 await api.internal.someModel.update(record.id, { title: "new title" });

The default logging methods are trace, debug, info, warn, error, and fatal.

Each logging method has the following signature: ([mergingObject], [message])

mergingObject (Object)

An object can optionally be supplied as the first parameter. Each enumerable key and value of the mergingObject is copied into the JSON log line.

logger.info({ MIX: { IN: true } });
// {"userspaceLogLevel":30,"time":1531254555820,,"MIX":{"IN":true}}
message (String)

A message string can optionally be supplied as the first parameter, or as the second parameter after supplying a mergingObject.

By default, the contents of the message parameter will be merged into the JSON log line under the msg key:

logger.info("hello world");
// {"userspaceLogLevel":30,"time":1531257112193,"msg":"hello world"}
logger.info({ MIX: { IN: true } }, "hello world");
// {"userspaceLogLevel":30,"time":1531257112193,"msg":"hello world","MIX":{"IN":true}}}

Inside any code effect, console.log is an instance of the logger object described above. Because the signature of the logger function is different from console.log, that can result in unexpected behaviour if you log an object after a message.

Querying logs

In the Gadget Log Viewer, you can query your logs using the UI, or you can write custom LogQL statements.


Gadget logs are queryable via LogQL. Check out the documentation to learn more. There are some basic parts of the query that Gadget will generate for you, and you should avoid editing them if possible:

The basic, default query looks like this, and will return all logs for the selected time range at 'INFO' level and above:

{app_logs_service=~".+", environment_id="{YOUR_ENVIRONMENT_ID"} | json | userspaceLogLevel>=30

Editing or removing the stream identifier below will most likely cause your query to have no results.

{app_logs_service=~".+", environment_id="{YOUR_ENVIRONMENT_ID"}

To reset, either close the Logs tab and re-open it, or click on the 'reset' icon in the query bar.

For best results, add any custom query you have to the end of this stream identifier.

Querying for a string

If you want to search your logs for a specific string, use the |= operator:

{app_logs_service=~".+", environment_id="{YOUR_ENVIRONMENT_ID"} | json | userspaceLogLevel>=30 |= "some string"
Excluding a string

If you want to search your logs, but exclude a specific string, use the != operator:

{app_logs_service=~".+", environment_id="{YOUR_ENVIRONMENT_ID"} | json | userspaceLogLevel>=30 != "some string"
Querying for a key/value pair

If you want to search your logs for a key/value combination of an object which you previously logged, use the | operator:

{app_logs_service=~".+", environment_id="{YOUR_ENVIRONMENT_ID"} | json | userspaceLogLevel>=30 | key="value"

Note that key/value search parameters must be preceded by the | json | operator which is already in the default query.

If you change or remove the default query, you will need to provide the | json | operation before your key/value search will work.