Skip to main content

Cloud Functions and Hooks Guide

info

This article focuses on a special use case of Cloud Engine that involves Cloud Functions and Hooks. To deploy general-purpose backend applications or learn more about the features provided by Cloud Engine, see Cloud Engine Platform Features.

Cloud Functions lets you run backend code on the cloud in response to various types of events. It is supported by our client-side SDKs and can automatically serialize objects that have the data types provided by our Data Storage service.

The following scenarios can be easily implemented with the help of Cloud Functions and Hooks:

  • Manage complex logics in one place without implementing them with different languages for each platform.
  • Adjust certain logics of your project on the server side without updating clients.
  • Retrieve and update data regardless of the ACL or class permissions.
  • Trigger custom logics or perform additional permission checks when objects are created, updated, or deleted, or when users are logged in or verified.
  • Run scheduled tasks to fulfill requirements like closing unpaid orders every hour, deleting outdated data every midnight, etc.

You can write Cloud Functions with any of the languages (runtime environments) supported by Cloud Engine, including Node.js, Python, Java, PHP, .NET, and Go. Our dashboard provides an online editor for you to write Cloud Functions in Node.js. If you prefer using another language, please create a project based on our demo project and deploy it to Cloud Engine.

Cloud Functions

Now let’s look at a more complex example. Assuming that we have an app for users to leave their reviews for movies. A Review object would look like this:

{
"movie": "A Quiet Place",
"stars": 5,
"comment": "I almost forgot breathing when watching this movie"
}

Here stars is a number between 1 and 5 that represents the score given by the reviewer. Let’s see how we can define a Cloud Function that helps us calculate the average score of a movie.

Cloud Functions accept requests in the form of JSON objects, which allows us to pass a movie’s name into a function when we invoke the function. Within the Cloud Function, we can use the Data Storage SDK to retrieve all the scores given to a movie. Now we have everything we need to implement our averageStars function:

AV.Cloud.define("averageStars", function (request) {
var query = new AV.Query("Review");
query.equalTo("movie", request.params.movie);
return query.find().then(function (results) {
var sum = 0;
for (var i = 0; i < results.length; i++) {
sum += results[i].get("stars");
}
return sum / results.length;
});
});

AV.Cloud.define accepts an optional parameter, options (placed between the function name and the function), which contains the following properties:

  • fetchUser?: boolean: Whether to automatically fetch the user logged in on the client side. Defaults to true. When set to false, Request will not contain the currentUser property.
  • internal?: boolean: Whether to only allow the function to be invoked within Cloud Engine (with AV.Cloud.run without enabling remote) or with the Master Key (by providing useMasterKey when calling AV.Cloud.run). When set to true, the function cannot be invoked directly by the client. Defaults to false.

For example, if we don’t want to allow clients to invoke the function defined above and we don’t care about the user logged in on the client side, the function above can be rewritten in the following way:

AV.Cloud.define(
"averageStars",
{ fetchUser: false, internal: true },
function (request) {
// Same as above
}
);

Parameters and Return Values

Request will be passed into the Cloud Function as a parameter. It contains the following properties:

  • params: object: The parameters sent from the client. When the Cloud Function is invoked with rpc, this could be an AV.Object.
  • currentUser?: AV.User: The user logged in on the client (according to the X-LC-Session header sent from the client).
  • sessionToken?: string: The sessionToken sent from the client (from the X-LC-Session header).
  • meta: object: More information about the client. For now, it only contains a remoteAddress property, which contains the IP address of the client.

If the Cloud Function returns a Promise, the resolved value of the Promise will be used as the response. When the Promise gives an error, the error will be used as the response instead. Error objects constructed with AV.Cloud.Error are considered client errors and will not be printed to the standard output. For other errors, call stacks will be printed to the standard output to help you debug your program.

We recommend that you build your program with Promise chains, which makes it easy for you to organize asynchronous tasks and handle errors. Please be sure to chain the Promises and return the chain within the Cloud Function.

Details about the early versions of the Node.js SDK

For Node.js SDK 2.0 and earlier, Cloud Function accepts two parameters: request and response. We will continue supporting this usage until the next major version, though it’s encouraged that you adopt Promise-style Cloud Functions for your project as soon as possible.

Invoking Cloud Functions With Client SDKs

You can invoke Cloud Functions with client SDKs:

try {
Dictionary<string, object> response = await LCCloud.Run("averageStars", parameters: new Dictionary<string, object> {
{ "movie", "A Quiet Place" }
});
// Handle result
} catch (LCException e) {
// Handle error
}

When running a Cloud Function, the arguments and responses will be treated as JSON objects. To pass LCObjects through requests and responses, you can invoke the Cloud Function with RPC, which makes the SDK serialize and deserialize LCObjects. This allows your program within the Cloud Function and on the client side to have access to the LCObjects directly:

try {
LCObject response = await LCCloud.RPC("averageStars", parameters: new Dictionary<string, object> {
{ "movie", "A Quiet Place" }
});
// Handle result
} catch (LCException e) {
// Handle error
}

When using RPC, the SDK will process the objects in the following forms that exist in the requests and responses:

  • Single LCObject
  • A HashMap containing LCObjects
  • An array containing LCObjects

Everything else in the requests and responses will remain in their original forms.

Invoking Other Cloud Functions Within a Cloud Function

When invoking a Cloud Function under the Node.js runtime environment, a local invocation will be triggered by default, which means that the invocation won’t make an HTTP request like what the client SDK would do.

AV.Cloud.run("averageStars", {
movie: "A Quiet Place",
}).then(
function (data) {
// Succeeded; data is the response
},
function (error) {
// Handle error
}
);

To force the invocation to make an HTTP request, provide the remote: true option. This is useful when you run the Node.js SDK in a different group or outside Cloud Engine:

AV.Cloud.run("averageStars", { movie: "A Quiet Place" }, { remote: true }).then(
function (data) {
// Succeeded
},
function (error) {
// Handle error
}
);

Here remote constitutes a part of the options object, which is an optional parameter of AV.Cloud.run. options contains the following parameters:

  • remote?: boolean: The remote option used in the example above. Defaults to false.
  • user?: AV.User: The user used to invoke the Cloud Function. Often used when remote is false.
  • sessionToken?: string: The sessionToken used to invoke the Cloud Function. Often used when remote is true.
  • req?: http.ClientRequest | express.Request: Used to provide properties like remoteAddress for the Cloud Function being invoked.

Error Codes for Cloud Functions

You can define custom error codes according to HTTP status codes.

AV.Cloud.define("customErrorCode", function (request) {
throw new AV.Cloud.Error("Custom error message.", { code: 123 });
});

The response gotten by the client would look like { "code": 123, "error": "Custom error message." }.

Timeouts for Cloud Functions

The default timeout for invoking a Cloud Function is 15 seconds. If the timeout is exceeded, the client will get a response with the HTTP status code 503 and the body being The request timed out on the server. Keep in mind that even though the client has already gotten a response, the Cloud Function might still be running, although its response will not be received by the client anymore (the error message Can't set headers after they are sent will be printed to the log). In certain cases, the client will receive the 524 or 141 error instead of the 503 error.

Avoiding Timeouts

To prevent Cloud Functions and scheduled tasks from causing timeouts, we recommend that you convert time-consuming tasks in your code to queued tasks that can be executed asynchronously.

For example, you can:

  1. Create a table in the Data Storage service with a column named status;
  2. When a task is received, create an object in the table with status being PROCESSING, then respond to the client with the object’s id.
  3. When a task is completed, update the status of the corresponding object to COMPLETED or FAILED;
  4. You can look up the status of a task with its id on the dashboard at any time.

The method introduced above might not apply to before hooks. Although you can ensure a before hook to complete within the timeout using the method above, its ability to abort an action when an error occurs will not work anymore. If you have a before hook that keeps exceeding the timeout, consider changing it to an after hook. For example, if a beforeSave hook needs to invoke a time-consuming API to tell if a user’s comment is spam, you can change it to an afterSave hook that invokes the API after the comment has been saved and deletes the comment if it is spam.

Hooks for Data Storage

Hooks are a special type of Cloud Functions that get triggered automatically by the system when certain events take place. Keep in mind that:

  • Importing data on the dashboard will not trigger any hooks.
  • Be careful not to form infinite loops with your hooks.
  • Hooks don’t work with the _Installation table.
  • Hooks only work with the classes that belong to the current application, which don’t include those bound to the current application.

For before hooks, if an error is returned by the function, the original operation will be aborted. This means that you can reject an operation by having the function throw an error. This does not apply to after hooks since the operation would’ve been completed when the function starts running.

graph LR A((save)) -->D{object} D-->E(new) E-->|beforeSave|H{error?} H-->N(No) N-->B[create new object on the cloud] B -->|afterSave|C((done)) H-->Y(Yes) Y-->Z((interrupted)) D-->F(existing) F-->|beforeUpdate|I{error?} I-->Y I-->V(No) V-->G[update existing object on the cloud] G-->|afterUpdate| C
graph LR A((delete))-->|beforeDelete|H{error?} H-->Y(Yes) Y-->Z((interrupted)) H-->N(No) N-->B[delete object on the cloud] B -->|afterDelete|C((done))

Our SDK will run authentication to ensure that the hook requests received by it are the legitimate ones sent from the Data Storage service. If the authentication fails, you may see a message saying Hook key check failed. If you see this message when debugging your project locally, please ensure that you have started your project with the CLI.

BeforeSave

This hook can be used to perform operations like cleanups and verifications before a new object gets created. For example, if we need to truncate each comment to 140 characters:

AV.Cloud.beforeSave("Review", function (request) {
var comment = request.object.get("comment");
if (comment) {
if (comment.length > 140) {
// Truncate the comment and add '…'
request.object.set("comment", comment.substring(0, 140) + "…");
}
} else {
// Don’t save the object and return an error
throw new AV.Cloud.Error("No comment provided!");
}
});

In the example above, request.object is the AV.Object we are performing our operation on. request has another property besides object:

  • currentUser?: AV.User: The user who triggered the operation.

The request parameter (and its properties) are available in other hooks as well.

AfterSave

This hook can be used to perform operations after a new object has been created. For example, if we need to update the total number of comments after a comment has been created:

AV.Cloud.afterSave("Comment", function (request) {
var query = new AV.Query("Post");
return query.get(request.object.get("post").id).then(function (post) {
post.increment("comments");
return post.save();
});
});

In the following example, we are adding a from property to each new user:

AV.Cloud.afterSave("_User", function (request) {
console.log(request.object);
request.object.set("from", "LeanCloud");
return request.object.save().then(function (user) {
console.log("Success!");
});
});

Although we don’t care about the return value of an after hook, we still recommend that you have your function return a Promise. This ensures that you can see error messages and call stacks in the standard output when unexpected errors occur.

BeforeUpdate

This hook can be used to perform operations before an existing object gets updated. You will be able to know which fields have been updated and reject the operation if necessary:

AV.Cloud.beforeUpdate("Review", function (request) {
// If `comment` has been updated, check its length
if (request.object.updatedKeys.indexOf("comment") != -1) {
if (request.object.get("comment").length > 140) {
// Reject the update if the comment is too long
throw new AV.Cloud.Error(
"The comment should be no longer than 140 characters."
);
}
}
});

Modifications done directly to the object passed in will not be saved. To reject the update, you can have the function return an error.

The object passed in is a temporary object not saved to the database yet. The object might not be the same as the one saved to the database in the end because there might be atomic operations like self-increments, array operations, and adding or updating relations that will happen later.

AfterUpdate

This hook might lead to infinite loops if you use it improperly, causing additional API calls and even extra charges. Please make sure to read Avoiding Infinite Loops carefully.

This hook can be used to perform operations after an existing object has been updated. Similar to BeforeUpdate, you will be able to know which fields have been updated.

AV.Cloud.afterUpdate("Review", function (request) {
if (request.object.updatedKeys.indexOf("comment") != -1) {
if (request.object.get("comment").length < 5) {
console.log(review.ObjectId + " looks like spam: " + comment);
}
}
});

BeforeDelete

This hook can be used to perform operations before an object gets deleted. For example, to check if an Album contains any Photos before it gets deleted:

AV.Cloud.beforeDelete("Album", function (request) {
// See if any `Photo` belongs to this album
var query = new AV.Query("Photo");
var album = AV.Object.createWithoutData("Album", request.object.id);
query.equalTo("album", album);
return query.count().then(
function (count) {
if (count > 0) {
// The `delete` operation will be aborted
throw new AV.Cloud.Error(
"Cannot delete an album if it still has photos in it."
);
}
},
function (error) {
throw new AV.Cloud.Error(
"Error " +
error.code +
" occurred when finding photos: " +
error.message
);
}
);
});

AfterDelete

This hook can be used to perform operations like decrementing counters and removing associated objects after an object has been deleted. For example, to delete the photos in an album after the album has been deleted:

AV.Cloud.afterDelete("Album", function (request) {
var query = new AV.Query("Photo");
var album = AV.Object.createWithoutData("Album", request.object.id);
query.equalTo("album", album);
return query
.find()
.then(function (posts) {
return AV.Object.destroyAll(posts);
})
.catch(function (error) {
console.error(
"Error " +
error.code +
" occurred when finding photos: " +
error.message
);
});
});

OnVerified

This hook can be used to perform operations after a user has verified their email or phone number. For example:

AV.Cloud.onVerified("sms", function (request) {
console.log("User " + request.object + " is verified by SMS.");
});

The object in the example above can be replaced by currentUser since the user who triggered the operation is also the one that we want to perform operations on. This also applies to the onLogin hook, which we’ll introduce later.

Fields like emailVerified should not be updated here since the system will update them automatically.

This hook is an after hook.

OnLogin

This hook can be used to perform operations before a user gets logged in. For example, to prevent users on the blocklist from logging in:

AV.Cloud.onLogin(function (request) {
// The user has not logged in yet, so the user data is in `request.object`
console.log("User " + request.object + " is trying to log in.");
if (request.object.get("username") === "noLogin") {
// If the program throws an error, the user won’t be able to log in (the client will receive a 401 error)
throw new AV.Cloud.Error("Forbidden");
}
});

This hook is a before hook.

OnAuthData

This hook gets triggered when a user’s authData gets updated. You can perform verifications and modifications to users’ authData with this hook. For example:

AV.Cloud.onAuthData(function (request) {
let authData = request.authData;
console.log(authData);

if (authData.weixin.code === "12345") {
authData.weixin.accessToken = "45678";
} else {
// Verification failed; an error will be thrown and the user won’t be able to log in
throw new AV.Cloud.Error("invalid code");
}
// Verification succeeded; the verified or modified `authData` will be returned and the user will be able to log in
return authData;
});

This hook is a before hook.

Avoiding Infinite Loops

You might be curious about why saving post in AfterUpdate won’t trigger the same hook again. This is because Cloud Engine has processed the object being passed in to prevent infinite loops.

However, this mechanism won’t work when one of the following situations happens:

  • Calling fetch on the object being passed in.
  • Reconstructing the object being passed in.

In these cases, you might want to call the method for disabling hooks yourself:

// Directly editing and saving an object won’t trigger the `afterUpdate` hook
request.object.set("foo", "bar");
request.object.save().then(function (obj) {
// Your code
});

// If `fetch` has been called on the object, call `disableAfterHook` on the new object to prevent the object from triggering the hook
request.object
.fetch()
.then(function (obj) {
obj.disableAfterHook();
obj.set("foo", "bar");
return obj.save();
})
.then(function (obj) {
// Your code
});

// If the object has been reconstructed, call `disableAfterHook` on the new object to prevent the object from triggering the hook
var obj = AV.Object.createWithoutData("Post", request.object.id);
obj.disableAfterHook();
obj.set("foo", "bar");
obj.save().then(function (obj) {
// Your code
});

Error Codes for Hooks

Use the following way to define error codes for hooks like BeforeSave:

AV.Cloud.beforeSave("Review", function (request) {
// Convert the object to a string with JSON.stringify()
throw new AV.Cloud.Error(
JSON.stringify({
code: 123,
message: "An error occurred.",
})
);
});

The response gotten by the client would look like Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }. The client can then slice the string to get the error message.

Timeouts for Hooks

The timeout for before hooks is 10 seconds while that for other hooks is 3 seconds. If a hook is triggered by another Cloud Function (like when a BeforeSave or AfterSave hook gets triggered when a new object gets created), its timeout will be cut down to the remaining timeout of the triggering Cloud Function.

For example, if a BeforeSave hook is triggered by a Cloud Function that has already been executed for 13 seconds, the hook will only have 2 seconds left. See Timeouts for Cloud Functions for more information.

Hooks for Instant Messaging

See Hooks and System Conversations for more information.

Writing Cloud Functions Online

You can write Cloud Functions online using the dashboard instead of creating and deploying a project. Keep in mind that:

  • Deploying the Cloud Functions you write online will override the project deployed with Git or the CLI.
  • You can only write Cloud Functions and hooks online. You can’t upload static web pages or write dynamic routes with the web interface.
  • You can only use the JavaScript SDK together with some built-in Node.js modules (listed in the table below). You can’t import other modules as dependencies.

Path

Writing Cloud Functions online

On Dashboard > LeanEngine > Manage deployment > Your group > Deploy > Edit online, you can:

  • Create function: When creating a Cloud Function, you can specify its type, name, code, and comments. Click Create to save the Cloud Function. Types of Cloud Functions include Function (basic Cloud Function), Hook, and Global (shared logic used by multiple Cloud Functions).
  • Deploy: You can select the environment to deploy your Cloud Functions to and click Deploy to proceed.
  • Preview: This will combine all the Cloud Functions into a single code snippet. You can verify the code in your Cloud Functions or override the file named cloud.js in a project created from the demo project with the code shown here.
  • Maintain Cloud Functions: You can edit existing Cloud Functions, view histories, and delete Cloud Functions.

After you edit your Cloud Functions, make sure to click Deploy to have the edits take effect.

The online editor only supports Node.js at this time. The latest version, v3, uses Node.js 8.x and the Node.js SDK 3.x. With this version selected, you’ll need to write your functions using Promise. Packages provided by default include async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, and xml2js.

More about the SDK versions used by the online editor
VersionNode.js SDKJS SDKNode.jsNotesDependencies available
v00.x0.x0.12Not recommended anymoremoment, request, underscore
v11.x1.x4async, bluebird, co, ejs, handlebars, joi, lodash, marked, moment, q, request, superagent, underscore
v22.x2.x6Please write your functions using Promiseasync, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js
v33.x3.x8Please write your functions using Promiseasync, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js

Upgrading from v0 to v1:

  • Upgraded the JS SDK to 1.0.
  • Requires you to obtain the user from request.currentUser instead of AV.User.current.
  • Requires you to manually provide the user object when calling AV.Cloud.run.

Upgrading from v1 to v2:

  • Upgraded the JS SDK to 2.0 (Promise must be used instead of callback).
  • Removed AV.Cloud.httpRequest.
  • Requires you to return a Promise from each Cloud Function and throw an AV.Cloud.Error for each error.

Upgrading from v2 to v3:

  • Upgraded the JS SDK to 3.0 (includes changes to the behavior of AV.Object.toJSON).
Difference between writing Cloud Functions online and deploying a project

When you write Cloud Functions online, the way our system deals with your functions is to join them together, generate a Cloud Engine project, and deploy this project.

You can consider writing Cloud Functions online to be the same as deploying a project: they both form a complete project in the end.

When you define functions online, you can quickly generate a Cloud Engine project without using our SDK or Git.

You may also choose to create and deploy a project written based on our SDK.

The two options are mutually exclusive: when you deploy with one option, the deployment using the other option will be overridden.

Migrating from writing Cloud Functions online to deploying a project
  1. Install the CLI according to the CLI Guide and create a new project by running lean new. Select Node.js > Express as the template (this is our demo project for Node.js).
  2. Navigate to Dashboard > LeanEngine > Manage deployment > Your group > Deploy > Edit online and click Preview. Copy the code shown here and replace the code in cloud.js with it.
  3. Run lean up. Now you can test your Cloud Functions and hooks on http://localhost:3001. Once you’re done, run lean deploy to deploy your code to Cloud Engine. If you have standard instances, make sure to run lean publish as well.
  4. After deploying your project, watch the dashboard to see if there are any errors.

If you have been using the Node.js SDK 0.x, you’ll have to update your code to avoid compatibility issues. For example, AV.User.current() should be changed to request.currentUser.

Viewing and Running Cloud Functions

Dashboard > LeanEngine > Manage deployment > Your group > Deploy shows all the Cloud Functions and hooks defined within each group, including their names, groups, and QPM (requests per minute). You can click Run to run a Cloud Function from the dashboard.

Cloud Functions from all the groups in your application will be displayed here regardless of the way they’re deployed (writing Cloud Functions online or deploying a project).

List of Cloud Functions

Production Environment vs. Staging Environment

Your app comes with a production environment and a staging environment. When triggering a Cloud Function from within a Cloud Engine instance using the SDK, no matter explicitly or implicitly (by triggering hooks), the SDK will trigger the function defined in the same environment as the instance. For example, with beforeDelete defined, if an object is deleted with the SDK in the staging environment, the beforeDelete hook in the staging environment will be triggered.

When triggering Cloud Functions outside of a Cloud Engine instance using the SDK, no matter explicitly or implicitly, X-LC-Prod will be set to 1 by default, which means that the Cloud Functions in the production environment will be triggered. For historical reasons, there are some differences among the SDKs:

  • For Node.js, PHP, Java, and C# SDKs, the production environment will be used by default.
  • For the Python SDK, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used.
  • For Java example projects, java-war-getting-started and spring-boot-getting-started, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used (same as the Python SDK).

You can specify the environment being used with the SDK:

LCCloud.IsProduction = true; // production (default)
LCCloud.IsProduction = false; // staging

If you’re using the trial mode, there will only be a production environment and you won’t be able to switch to the staging environment.

Scheduled Tasks

You can set up scheduled tasks to run your Cloud Functions periodically. For example, you can have your app clean up temporary data every night, send push notifications to users every Monday, etc. The time set for a scheduled task can be accurate to a second.

The timeout applied to ordinary Cloud Functions also applies to scheduled tasks. See Avoiding Timeouts for more information.

If a scheduled task triggers more than 30 400 (Bad Request) or 502 (Bad Gateway) errors within 24 hours, the task will be disabled and you will get an email regarding the issue. The error timerAction short-circuited and no fallback available will also be printed to the log.

After deploying your program to Cloud Engine, go to Dashboard > LeanEngine > Manage deployment > Your group > Scheduled tasks and click Create scheduled task to create a scheduled task for a Cloud Function. For example, if we have a function named logTimer:

AV.Cloud.define("logTimer", function (request) {
console.log("This log is printed by logTimer.");
});

List of scheduled tasks

You can specify the times a scheduled task gets triggered using one of the following expressions:

  • CRON expression
  • Interval in seconds

Take the CRON expression as an example. To print logs at 8am every Monday, create a scheduled task for the function logTimer using a CRON expression and enter 0 0 8 ? * MON for it.

See Cloud Queue Guide § CRON Expressions for more information about CRON expressions.

When creating a scheduled task, you can optionally fill in the following two fields:

  • Params: The arguments passed to the Cloud Function as a JSON object.
  • Error-handling policy: Whether to retry or cancel the task when it fails due to a Cloud Function timeout. See Cloud Queue Guide § Error-Handling Policy for more information.

Last execution is the time and result of the last execution. This information will only be retained for 5 minutes. Under the details of the execution:

  • status: The status of the task; could be success or failed
  • uniqueId: A unique ID for the task
  • finishedAt: The exact time when the task finished (for succeeded tasks only)
  • statusCode: The HTTP status returned by the Cloud Function (for succeeded tasks only)
  • result: The response body returned by the Cloud Function (for succeeded tasks only)
  • error: Error message (for failed tasks only)
  • retryAt: The time the task will be rerun (for failed tasks only)

You can view the logs of all the scheduled tasks on Dashboard > LeanEngine > Manage deployment > Your group > Logs.