Smart Document Automation:: From Zoho Projects to Zoho Writer – Merge, Edit, and Share

Smart Document Automation:: From Zoho Projects to Zoho Writer – Merge, Edit, and Share

Hello Everyone, 

A custom function is a software code that can be used to automate a process and this allows you to automate a notification, call a webhook, or perform logic immediately after a workflow rule is triggered. This feature helps to automate complex tasks and calculations.

In this post, I will present a specific use case that demonstrates the integration between Zoho Projects and Zoho Writer.


Use Case:

Merge data from Zoho Projects into a predefined template in Zoho Writer.
Automatically generate a fillable document.
Share the document link with a designated user, either specified within Zoho Writer or Zoho Projects.
Include fillable fields within the merge template, allowing the recipient to complete the necessary information.

This functionality has been successfully implemented using Custom Functions. The following steps outline the setup process. Refer to the attached screenshots for additional context.

Step 1: Navigate to Zoho Writer → Automate → Merge Template
Step 2: Click on 'Convert'
Step 3: Select a data source to start merging
Step 4: Select 'Custom Source'
Step 5: Click on 'Use' next to the Projects Fields
Step 6: Once done, enter the Zoho Org ID i.e., ZOID within the portalID.
Step 7: Click on 'Connections' within the same page → Create a Connection with the mentioned scopes.
Step 8: Click on 'Manage Fields' → Drag and Drop the required fields.
Step 9: You can setup the Merge options as per your concern as in this screen shot
Step 10: Click on 'Merge & Share Fillable Link'
Step 11: Select the required field from the 'Fields & Buttons'
Step 12: Configure the 'Merge & share fillable link'
Step 13: Click on 'Configure from submit actions' → Webhook & Custom Functions → Execute Custom Functions.

The following functionality can be achieved using the code provided below. Additionally, two connections must be created as outlined

  1. Zoho Projects Connection :: Create a connection for the Zoho Projects service with the following scopes 'ZohoProjects.projects.READZohoProjects.milestones.READZohoProjects.tasklists.READZohoProjects.tasks.READZohoProjects.users.READ, and ZohoProjects.clients.READ'.

    Update the placeholder 'xxxxxxxxx' with the actual connection name in ZPConnection.

  2. Zoho OAuth Connection :: Create a connection for the Zoho OAuth service with the following scopes 'ZohoWriter.documentEditor.ALL and ZohoWriter.Merge.ALL'.

    Replace 'yyyyyyyyy' with the actual connection name in ZWConnection.

For detailed guidance on creating a connection, please refer to this link. The required code is provided below.

//connections for authentication
tomailaddress = "a@b.com";
ZPConnection = "xxxxxxxxx";
ZWConnection = "yyyyyyyyy";
projectsDomain = "https://projectsapi.zoho.com/";
writerDomain = "https://zohoapis.com/";
headers = Map();
// Mandatory header - Do not remove
headers.put("x-zoho-service","ZohoWriter");
//Domain url for Projects and Writer
projectsDetailsUrl = projectsDomain + "restapi/portal/" + portalId + "/projects/" + projectId + "/";
milestoneDetailsUrl = projectsDomain + "restapi/portal/" + portalId + "/projects/" + projectId + "/milestones/";
taskListDetailsUrl = projectsDomain + "restapi/portal/" + portalId + "/projects/" + projectId + "/tasklists/";
taskDetailsUrl = projectsDomain + "restapi/portal/" + portalId + "/projects/" + projectId + "/tasks/";
projectsUsersUrl = projectsDomain + "/api/v3/portal/" + portalId + "/projects/" + projectId + "/users";
clientUsersUrl = projectsDomain + "api/v3/portal/" + portalId + "/projects/" + projectId + "/clients/users";
mergeFieldsUrl = writerDomain + "/writer/api/v1/documents/" + templateId + "/fields";
mergeAndStoreUrl = writerDomain + "/writer/api/v1/documents/" + templateId + "/merge/sharetofill";
projectsCommentUrl = projectsDomain + "api/v3/portal/" + portalId + "/projects/" + projectId + "/comments";
getAllFields = invokeurl
[
url :mergeFieldsUrl
type :GET
connection:"writer"
];
if(!getAllFields.contains("error") && getAllFields.getJSON("merge").size() > 0)
{
values_map = Map();
projectDetails = invokeurl
[
url :projectsDetailsUrl
type :GET
headers:headers
connection:"connectionprojects"
];
if(!projectDetails.contains("error"))
{
projectFieldValues = projectDetails.getJson("projects").get(0);
projectCF = false;
if(projectFieldValues.contains("custom_fields"))
{
projectCF = true;
projectCustomfields = projectFieldValues.getJSON("custom_fields");
}
for each  mergeField in getAllFields.getJson("merge")
{
if(projectFieldValues.containsKey(mergeField.getJSON("id")))
{
if(mergeField.getJSON("id") == "TAGS")
{
tagList = List();
for each  tag in projectFieldValues.getJSON("TAGS")
{
tagList.add(tag.getJSON("name"));
}
values_map.put(mergeField.get("id"),tagList);
continue;
}
else
{
values_map.put(mergeField.get("id"),projectFieldValues.getJSON(mergeField.getJSON("id")));
continue;
}
}
// merge project customfields
if(projectCF == true)
{
for each  field in projectCustomfields
{
if(field.containsKey(mergeField.getJSON("id")))
{
values_map.put(mergeField.get("id"),field.getJSON(mergeField.getJSON("id")));
break;
}
}
}
// merge milestone details
if(mergeField.get("id") == "milestone_details")
{
milestoneDetilList = List();
MilestoneDetails = invokeurl
[
url :milestoneDetailsUrl
type :GET
headers:headers
connection:"connectionprojects"
];
if(!MilestoneDetails.contains("error"))
{
allMilestonedetails = MilestoneDetails.getJson("milestones");
for each  milestone in allMilestonedetails
{
milestoneDetilMap = Map();
for each  milestoneField in mergeField.getJSON("fields")
{
mergeFieldId = milestoneField.get("id");
mergeFieldName = mergeFieldId.subString("milestone_details.".length(),mergeFieldId.length());
//merge milestone fields
if(milestone.containsKey(mergeFieldName))
{
if(mergeFieldName == "tags")
{
tagList = List();
for each  tag in milestone.getJSON(mergeFieldName)
{
tagList.add(tag.getJSON("name"));
}
milestoneDetilMap.put(mergeFieldId,tagList);
continue;
}
else if(mergeFieldName == "status")
{
milestoneDetilMap.put(mergeFieldId,milestone.getJson("status_det").getJson("name"));
continue;
}
else
{
milestoneDetilMap.put(mergeFieldId,milestone.getJson(mergeFieldName));
continue;
}
}
}
milestoneDetilList.add(milestoneDetilMap);
}
values_map.put("milestone_details",milestoneDetilList);
}
}
// merge tasklist details
if(mergeField.get("id") == "tasklist_details")
{
taskListDetilList = List();
getTasklistDetails = invokeurl
[
url :taskListDetailsUrl
type :GET
headers:headers
connection:"connectionprojects"
];
if(!getTasklistDetails.contains("error"))
{
taskListdetails = getTasklistDetails.getJson("tasklists");
for each  tasklist in taskListdetails
{
taskListDetilMap = Map();
for each  taskListField in mergeField.getJSON("fields")
{
mergeFieldId = taskListField.get("id");
mergeFieldName = mergeFieldId.subString("tasklist_details.".length(),mergeFieldId.length());
//merge tasklist fields
if(tasklist.containsKey(mergeFieldName))
{
if(mergeFieldName == "tags")
{
tagDetails = List();
for each  tag in tasklist.getJson(mergeFieldName)
{
tagDetails.add(tag.getJSON("name"));
}
taskListDetilMap.put(mergeFieldId,tagDetails);
continue;
}
else
{
taskListDetilMap.put(mergeFieldId,tasklist.getJson(mergeFieldName));
continue;
}
}
}
taskListDetilList.add(taskListDetilMap);
}
values_map.put("tasklist_details",taskListDetilList);
}
}
// merge task details
if(mergeField.get("id") == "task_details")
{
taskDetilList = List();
getTaskDetails = invokeurl
[
url :taskDetailsUrl
type :GET
headers:headers
connection:"connectionprojects"
];
if(!getTaskDetails.contains("error"))
{
taskdetails = getTaskDetails.getJson("tasks");
for each  taskdetail in taskdetails
{
taskDetilMap = Map();
taskCFs = false;
if(taskdetail.contains("custom_fields"))
{
taskCFs = true;
taskCustomfields = taskdetail.getJSON("custom_fields");
}
for each  taskField in mergeField.getJSON("fields")
{
mergeFieldId = taskField.get("id");
mergeFieldName = mergeFieldId.subString("task_details.".length(),mergeFieldId.length());
//merge task fields
if(taskdetail.containsKey(mergeFieldName))
{
if(mergeFieldName == "tasklist" || mergeFieldName == "status")
{
taskDetilMap.put(mergeFieldId,taskdetail.getJson(mergeFieldName).getJson("name"));
continue;
}
else if(mergeFieldName == "tags")
{
tagDetails = List();
for each  tag in taskdetail.getJson(mergeFieldName)
{
tagDetails.add(tag.getJSON("name"));
}
taskDetilMap.put(mergeFieldId,tagDetails);
continue;
}
else
{
taskDetilMap.put(mergeFieldId,taskdetail.getJson(mergeFieldName));
continue;
}
}
else if(taskdetail.containsKey("details"))
{
if(mergeFieldName == "owners")
{
ownerDetail = List();
ownerList = taskdetail.getJson("details").getJSON("owners");
for each  owner in ownerList
{
ownerDetail.add(owner.getJson("name"));
}
taskDetilMap.put(mergeFieldId,ownerDetail);
continue;
}
}
else if(taskdetail.containsKey("log_hours"))
{
if(mergeFieldName == "billable_hours" || mergeFieldName == "non_billable_hours")
{
taskDetilMap.put(mergeFieldId,taskdetail.getJson("log_hours").getJSON(mergeFieldName));
continue;
}
}
//merge task customfields
if(taskCFs == true)
{
for each  taskCF in taskCustomfields
{
if(taskCF.containsKey(mergeFieldName))
{
taskDetilMap.put(mergeFieldId,taskCF.getJson(mergeFieldName));
break;
}
}
}
}
taskDetilList.add(taskDetilMap);
}
values_map.put("task_details",taskDetilList);
}
}
// merge projectUsers
if(mergeField.get("id") == "project_users")
{
projectUserList = List();
getProjectUsers = invokeurl
[
url :projectsUsersUrl
type :GET
connection:"connectionprojects"
];
if(!getProjectUsers.contains("error"))
{
projectUserdetails = getProjectUsers.getJson("users");
for each  userDetails in projectUserdetails
{
projectUsersMap = Map();
for each  userField in mergeField.getJSON("fields")
{
mergeFieldId = userField.get("id");
mergeFieldName = mergeFieldId.subString("project_users.".length(),mergeFieldId.length());
//merge projectusers fields
if(mergeFieldName == "role")
{
if(userDetails.get(mergeFieldName).size() > 0)
{
projectUsersMap.put(mergeFieldId,userDetails.getJSON(mergeFieldName).getJSON("name"));
continue;
}
}
else if(mergeFieldName == "profile")
{
if(userDetails.get(mergeFieldName).size() > 0)
{
projectUsersMap.put(mergeFieldId,userDetails.getJSON(mergeFieldName).getJSON("name"));
continue;
}
}
else
{
projectUsersMap.put(mergeFieldId,userDetails.getJSON(mergeFieldName));
continue;
}
}
projectUserList.add(projectUsersMap);
}
values_map.put("project_users",projectUserList);
}
}
// merge clients users
if(mergeField.get("id") == "client_users")
{
clientUserList = List();
getProjectClientUser = invokeurl
[
url :clientUsersUrl
type :GET
connection:"connectionprojects"
];
if(!getProjectClientUser.contains("error"))
{
clientUserdetails = getProjectClientUser.getJson("clients").getJSON("users");
for each  userDetails in clientUserdetails
{
clientUsersMap = Map();
for each  userField in mergeField.getJSON("fields")
{
mergeFieldId = userField.get("id");
mergeFieldName = mergeFieldId.subString("client_users.".length(),mergeFieldId.length());
// merge clientsUsers
if(!mergeFieldName == "profile")
{
clientUsersMap.put(mergeFieldId,userDetails.getJSON(mergeFieldName));
continue;
}
else
{
clientUsersMap.put(mergeFieldId,userDetails.getJSON("profile").getJson("name"));
continue;
}
}
clientUserList.add(clientUsersMap);
}
values_map.put("client_users",clientUserList);
}
}
}
mergeDataParams = Map();
mergeDataParams.put("merge_data",{"data":values_map});
mergeAndStore = invokeurl
[
url :mergeAndStoreUrl
type :POST
parameters:mergeDataParams
connection:"writer"
];
if(!mergeAndStore.containsKey("error") && mergeAndStore.containsKey("merge_report_url"))
{
   mailMessage =  "Fill in the document " + mergeAndStore.get("records").get(0).get("fillable_link");
sendmail
[
from :zoho.loginuserid
to :tomailaddress
subject :"Fillable document link"
message :mailMessage
]
}
return mergeAndStore;
}
else
{
return projectDetails;
}
}
else
{
return getAllFields;
}

Creating custom functions in Zoho Projects is a straightforward process, supported by comprehensive documentation. Zoho offers a variety of built-in functions that serve as useful starting points, and you can also define your own functions using Zoho’s scripting language, Deluge. Implementing these functions can significantly enhance efficiency and improve productivity.

Stay tuned for additional examples and custom function scripts.

    • Sticky Posts

    • Time Log Reminder

      Tracking the time spent on tasks and issues is one of the most important functions of a timesheet. However, users may forget to update the time logs because they have their own goals to achieve. But, time logs must be updated at regular intervals to keep
    • Introducing the Zoho Projects Learning Space

      Every product has its learning curve, and sometimes having a guided path makes the learning experience smoother. With that goal, we introduce a dedicated learning space for Zoho Projects, a platform where you can explore lessons, learn at your own pace,
    • Update on V2 API End-of-Life Timeline

      Dear Users, Earlier this year, we shared the launch of the V3 APIs and requested users to migrate from the older V2 APIs by December 2025. We have received valuable feedback from our users and partners regarding their migration timelines. We are happy
    • Automation Series: Auto-update Phase Status

      Hello Folks! You can auto-update your phase's status based on status of underlying tasks using custom functions. In this series, we will showcase how to create and run custom functions, using Deluge, with ease. Follow the steps below and automate your
    • Automate Timesheet Approvals with Multi-level Approval Rules

      Introducing Approval Rules for Timesheets in Zoho Projects. With this automation, teams can manage how timesheets are reviewed and approved by setting up rules with criteria and assigning approvers to handle submissions. Timesheet, when associated to
    • Recent Topics

    • Edit Default Print

      What I want to do is create / edit the "Default Print" template without changing the layout I'm using. Currently I can create "Email Templates" but I don't use them for emailing and getting to that selection takes multiple clicks. If an email template could be set to be the Default Print template that would be helpful. I'd also like to print these in bulk but don't see that option (there is a mail merge macro but that goes straight to emailing, not to print or PDF).
    • Widget shows error

      Hi, May I ask why below characters will be auto added into the widget link and how to solve this error? As my widget shows 404 error now. ?serviceOrigin=https%3A%2F%2Fcrm.zoho.com&frameorigin=https%3A%2F%2Fcrm.zoho.com
    • How to print envelope labels from Zoho CRM

      Can anybody give me any clue how to print envelope/package labels directly from Contacts view? Regards, Alexandru Moderation Update: The Canvas Print View, which also facilitates the formatting of mailing labels, is now available! Learn more here: Zoho
    • Data Import: New interface, improved field mapping, and more

      Hello everyone! We’ve redesigned the data import process to ensure accurate field alignment and verify that every column in the uploaded file maps correctly with Desk fields before import. Here's how: Streamlined Import Steps Importing data is now more
    • Address labels

      Hi, we've been searching for a long time for a simple way for our employees to print address labels using a Dymo Label Writer 450. We came up with two alternatives that work, bur aren't really optimal.  First one is WebMerge. Webmerge is a great application that does 100% what we need but it is way too expensive for our needs. We only need to print a couple of labels per month. The second one is the integrated print tool in Zoho CRM, (Print preview) You would think this is the obvious way to do this
    • Print Labels

      How can I configure my address to print my labels like this: Name Mailing Address Mailing City, Mailing State Mailing Zip Country Instead of: Name Mailing Address Mailing City Mailing State Mailing Zip Country Thanks!!!!
    • Mailing labels - Improperly formatted

      All I'm trying to do is print one, properly formatted, mailing label. I'm in the Contacts module. In the default "list view" for mailing labels I selected a single client, hit the "more actions" drop down and selected print mailing label. Unfortunately,
    • Printing mailing labels

      The ability to print mailing labels would seem to be an important, basic, function of a good CRM. I find it very surprising that this has still not been addressed adequately by Zoho. When trying to use the existing "mailing label" included: 1. the data
    • Pageless mode needed to modernise Writer

      When we switched from GSuite to Zoho, one of the easiest apps I found to give up, was Docs. In many ways, Writer has always been more powerful than Docs, especially in terms of workflows/fillable forms/etc. However, I went back into Docs because I notice
    • Zoho Projects - Visual improvement to parent and sub-task relationship

      Hi Projects Team, My feature request is to improve sub-task visibility. Please see screenshot below. I really think parent child relationships could be visually improved. Even if the first letter of the parent task was inline with other same level tasks
    • API method to get activity feed in Recruit

      Hi community, I'm trying to figure out - is there any API method tto get information about datetime when Recruit/Candidates record tag where added?
    • Printing Mailing labels

      Is there any way to adjust the size of the printing labels? or product would I use to print labels from Zoho? Thanks, Josef Krieger Moderation Update (14th April 2025): We have another post discussing the same topic with votes and feedback from users.
    • Default to Current Date

      I'm importing data from a excel spread sheet that does not have date column and I'd like the date column in the Zoho Database to default to the current date. Any way I can do this?
    • Different Company Name for billing & shipping address

      We are using Zoho Books & Inventory for our Logistics and started to realize soon, that Zoho is not offering a dedicated field for a shipping address company name .. when we are creating carrier shipping labels, the Billing Address company name gets always
    • Make Packages from multiple sales order of a single customer

      Our customers sends orders to us very frequently, some times what customer wants is to ship items from 5 to 6 sales orders in a single shipment. it will be very nice if, zoho can implement this function, in which we can select items from other sales orders of the customer.
    • Show Custom Button in Portal Listview Canvas

      I have created a custom button that shows in a list view of deals (internally I can see it). I have permissions to allow this button on the portal. But it is not displaying in the canvas list? Before I do too much leg work, is this function allowed?
    • Zoho Sites "pages" management page

      I have 80 plus pages on zoho sites. When I go to the "pages" link to view and edit pages, They are not in any kind of order, so I spend lots of time searching for pages when I need to edit or create new. How can I change the view order of all my pages
    • AI feature in Zoho Desk suggesting answers based on past ticket threads

      Hi I would like to suggest something that would be very useful : instead of suggesting answers based on the Knowledge Base, I think it would be great if Zia could analyze the history of all customer and agents threads, to suggest answers in new tickets.
    • Advanced Customization of the Help Center using JavaScript

      Hello everyone, The Help Center in Zoho Desk can be customized by using HTML and CSS to provide structure and enhance the page's appearance—but what if you want to add interactive and dynamic elements? You can add these effects with JavaScript, a programming
    • Introducing Zoho POS for the Kingdom of Saudi Arabia

      Hey everyone, We are excited to kick-start December with a completely personalized edition of POS for retail businesses in Saudi Arabia to help run your operations with ease. It offers four different subscription plans—Free, Standard, Professional, and
    • Unveiling Zoho Sites 2.0 - A new dimension in website building

      Dear Zoho Sites Users, We are thrilled to announce the launch of Zoho Sites 2.0 today! This refresh represents a significant step forward in the capabilities of Zoho Sites and is crucial for creating a lasting and positive impact on our customers' businesses.
    • Script Editor not an option

      I am trying to apply a script to a sheet and Script Editor is not an option. I don't want to go outside Sheets to do this (like Creator) if it can be done inside Sheets.
    • Not able to link email text.

      Kindly check Zoho Sites. I am unable to turn a text into a email link. The save button does not work. Kindly try yourself to see it not responding to save.
    • Migrate data from old to new account

      Hy, Have one Old Zoho Notebook Account with Data , want to migrate that whole Data to New Zoho Notebook Account which is in Zoho One . Is that possible ? If Yes then how?
    • Zoho Analytics Regex Support

      When can we expect full regex support in Zoho Analytics SQL such as REGEXP_REPLACE? Sometimes I need to clean the data and using regex functions is the easiest way to achieve this.
    • Add Custom Reports To Dashboard or Home Tab

      Hi there, I think it would be great to be able to add our custom reports to the Home Tab or Dashboards. Thanks! Chad
    • landed cost-need help with different currency under the same bill

      I’m having trouble recording landed costs in Zoho Inventory/Books. My purchase order is in CNY, but the landed cost (freight) I pay is in USD. Zoho forces everything under the same bill to use one currency, so I can’t enter the landed cost in its actual
    • Pricing Strategies: #1 Nuances in Pricing

      When Clara first opened her digital printing shop, pricing was simple. She sold handmade greeting cards, planners, business cards, and other physical items at fixed label prices, individually and in bulk. One SKU, one price, one bill, and that's all it
    • [Free Webinar] Learning Table Series – Education Management in Zoho Creator

      Hello Everyone! We’re excited to invite you to another edition of Learning Table Series, where we showcase how Zoho Creator empowers industries with innovative and automated solutions. About the Learning Table Series The Learning Table Series is a free,
    • Move attachments from one module to another with Deluge

      I have created a button that works just like the convert button for my custom modules. I would like this custom function to move any attachments in this record to the new module. I can't seem to find any documentation on how this can be accomplished.
    • Help in function code

      Hi, could someone look at the code below and tell me what 's wrong with it? After a deal creation or edition the code should find the related Account, than all open Deals for that Account and copy the field Total_Open_Deals from the Account record to
    • Automation Series: Auto-update Phase Status

      Hello Folks! You can auto-update your phase's status based on status of underlying tasks using custom functions. In this series, we will showcase how to create and run custom functions, using Deluge, with ease. Follow the steps below and automate your
    • how to add subform over sigma in the CRM

      my new module don't have any subform available any way to add this from sigma or from the crm
    • Zoho Projects - Project Details on the Project Menu

      Hi Project's team, I've helped may businesses setup and use Zoho Project and one thing I see time and time again is confusion on where to find the Project Details information. I would be much more intuitive if Project Details was on the menu before Dashboard.
    • Zoho Projects - Add Feed to Project Tabs

      Hi Projects Team, I'm working on a lightweight communications requirement for one of my customers in relation to communicating with their client users via Zoho Projects. I noticed that the Feed is only available in the Collaboration section, but you can
    • Flow - Fetch info from drop down in another module

      I am running into a road block which I thought would be a simple task. My goal - The account is assigned to a "route" which can be selected from a drop down menu and adds a tag to the account accordingly (easy enough). Now when I create a task for this
    • Show unsubscribed contacts ?

      Hello, I would like to display the unsubscribed contacts. Unfortunately, I do not have this subscription type as described in the documentation (https://help.zoho.com/portal/en/kb/marketing-automation-2-0/user-guide/contacts/contact-management/articles/subscription-type-24-1-2024#Subscription_Type_field.)
    • Zoho Developer Community Hackathon 2025 is LIVE!

      Hey developers! It’s that time of the year again — the Zoho Developer Community Hackathon 2025 is officially open for registrations! If you’ve been waiting for a chance to stretch your skills, try something new, or finally bring that idea to life, this
    • Converted Leads Not Showing in Lead Reports

      Converted leads are not showing in the Lead reports. How can I make converted leads visible in the report,
    • Text widgets in dashboards

      Having a text widget in a dashboard would help immensely. It would allow adding links to related documents, relevant CRM views, etc. It would allow adding explanations of the data displayed in the other widgets, about how to interpret them or about filtering.
    • Next Page