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

    • 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
    • Accessibility Spotlight Series - 1

      Every user interacts with products differently, what feels intuitive to one may be challenging for another. Addressing this, accessibility is built into Zoho Project's design philosophy. This helps users navigate and perform actions with ease irrespective
    • Customize User Invites with Invitation Templates

      Invitation Templates help streamline the invitation process by allowing users to create customized email formats instead of sending a one-size-fits-all email. Different invitation templates can be created for portal users and client users to align with
    • Zoho Projects - Q3 Updates | 2025

      Hello Users, The final quarter of the year 2025 has begun, and we at Zoho Projects are all set with a plan. New targets to achieve and new milestones to reach, influenced by the lasting imprint of the past quarter. 2025's Q3 saw some new features and
    • Introducing Version-3 APIs - Explore New APIs & Enhancements

      Happy to announce the release of Version 3 (V3) APIs with an easy to use interface, new APIs, and more examples to help you understand and access the APIs better. V3 APIs can be accessed through our new link, where you can explore our complete documentation,
      • Recent Topics

      • Samsung Keyboard Issues with Notebook

        Dear Users, We're sorry to inform you that some of our users are experiencing certain issues like scrolling, delay/lag, cursor placement especially within the text notes. This occurs mainly due to Samsung Keyboard compatibility with Android 13 and some
      • Zoho Books | Product updates | October 2025

        Hello users, We’ve rolled out new features and enhancements in Zoho Books. From iOS 26 updates to viewing reports as charts, explore the updates designed to enhance your bookkeeping experience. Zoho Books Updates for Apple Devices At WWDC 2025, Apple
      • 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
      • Zoho Books | Product updates | September 2025

        Hello users, We’ve rolled out new features and enhancements in Zoho Books. From PayNow payment method to applying journal credits to invoices and bills in other locations, explore the updates designed to enhance your bookkeeping experience. Integrate
      • Contact data removes Account data when creating a quote

        Hi, Our customer has address fields in their quote layout which should be the address of the Account. They prefill the information, adding the account name - the address data is populated as per what is in the account - great. However when they then add
      • filter broke my data

        I uploaded a file recently from Sheets and it has top 2 rows frozen, with table headers in second row and each one is filterable. somehow my first 2 columns became unfiltered and no matter what I do I cannot reapply the filter?? also didn't realize they
      • Zoho Workdrive for Office, "vsto runtime not found"

        Hi all, I have been trying to get ZohoWorkdrive_MS-addin_1.4.exe installed, but I keep getting the error "VSTO Runtime Not Found!" - even though I have installed it ... Anyone else hear had problems with the MS addin?  FYI, I am using O365 on A Dell laptop running Win 10 Home - fully patched and up-to-date.  I have tried compatibility modes and running explicitly as Administrator - the usual steps. Any advice would be appreciated.
      • Add "Fetch Composite Item" Action for Inventory

        I want to make a Flow that uses information returned in the GET call for Composite Items, and it's not currently available in Zoho Flow. Please consider adding this functionality.
      • Inactive Items - Make Less Prominent by Default

        Currently, when one marks an Item as "Inactive", it really doesn't do much of anything to hide it or get it out of the way. Search and reporting within Finance should, by default, hide inactive Items from standard reports, searches, etc. If one specifically
      • This domain is not allowed to add. Please contact support-as@zohocorp.com for further details

        I am trying to setup the free version of Zoho Mail. When I tried to add my domain, theselfreunion.com I got the error message that is the subject of this Topic. I've read your other community forum topics, and this is NOT a free domain. So what is the
      • Why is Zoho Meeting quality so poor?

        I've just moved from Office 365 to Zoho Workplace and have been generally really positive about the new platform -- nicely integrated, nice GUI, good and easy-to-understand control and customisation, and at a reasonable price. However, what is going on
      • What functionality is new in the Z025 Spaces UI dashboard compared to the previous Zoho One dashboard?

        My company is interested in creating a revamped dashboard this coming year, and are trying to weigh different platform options. The new Spaces UI is said to be a more "feature-rich" dashboard experience than the previous. Could a developer please explain
      • Can no longer export as pdf

        In NOTEBOOK I am no longer able to export notes to pdfs as I used to be able to. When I hit that command it asks where I want to send it, but no matter what I chose there is no pdf attachment that shows up in order to send. Nothing happens.
      • Emails are going to notification folder and not in inbox

        emails are going to notification folder and not into inbox
      • 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
      • How can I prevent the Zoho Vault extension from automatically logging me out?

        Hi, I want the Zoho Vault Chrome extension to never log out. How can I do this? It only allows me to log out for 1 week, and the same applies to the Android app.
      • Automated log-out/session end

        I'm concerned about security of our data. Is it possible to set an automatic time-out for user sessions on Zoho CRM, after a certain period of inactivity or when the session reaches a certain duration (12 hours perhaps)? 
      • Multiple currencies - doesn’t seem to work for site visitors / customers

        I am trying to understand how the multiple currency feature works from the perspective of the website visitor who is shopping on my Zoho Commerce site. My site’s base currency is US Dollars (USD) but my store is for customers in Costa Rica and I would
      • Zoho Sites - contact us form

        I have added a contact us form to my website. I wanted to change the layout so I deleted the section of Contact Us form. When I again try to add the section of Contact Us form, it shows error that "This form already exists on this page."  I am unable
      • Zoho CRM App - Links

        Hi  Is there are plan for making Custom Links available in the Zoho CRM Android App? I can't see them being added? Thanks Gene
      • Templates

        Trying to sort out / get rid of unwanted invoice templates, the error msgs are not at all helpful. Surely it's easy to amend the error msg by including a list of the names of the customers / vendors that use it, denying deletion or making it inactive
      • Contacts limit in basic vs standard - what counts? Are customers contacts?

        I’ve been using books for a number years for my small business. I only ever work with 20 clients at any given time. I do purchase services from a number of vendors to run my business, so there are some comtacts there too. I used to use the basic package,
      • Finding missing records

        I have a challenge and I am not really sure where to start with it. I can't find any similar threads on here, can anyone help: I have two forms, FormA and FormB. Both forms have records that contain a field called Job_Number. What I am trying to achieve
      • Read webpage - MSXML2.ServerXMLHTTP

        I have the following VBA script, put together from various sources (mainly zoho forum/help/support, so it once worked, I guess): private Sub GetListOfSheets() Dim url As String Dim xmlhttp As Object Dim parameters As String Dim html As String range("B1").value
      • Addin Support in Zoho Sheet

        Is there any addin support available in zoho sheet as like google marketplace to enhance productivity by connecting with other apps, providing AI data analysis, streamlining business processes, and more?
      • Scheduled Reports - Do not send empty report

        Hello, We are intensively using reports in the CRM, especially for sales managers.  When data is empty, they still receive an email. Can you add an option to avoid sending the report when data is empty?
      • Prevent Unapproved Quotes from Exporting to Zoho CRM Finance Module

        Is it possible to prevent unapproved quotes in Zoho Books from being exported from Zoho Finance module inside Zoho CRM?
      • ZOHO BOOKS - RECEIVING MORE ITEMS THAN ORDERED

        Hello, When trying to enter a vendor's bill that contains items with bigger quantity than ordered in the PO (it happens quite often) - The system would not let us save the bill and show this error: "Quantity recorded cannot be more than quantity ordered." 
      • Updates for Zoho Campaigns: Merge tag, footer, and autoresponder migration

        Hello everyone, We'd like to inform you of some upcoming changes with regard to Zoho Campaigns. We understand that change can be difficult, but we're dedicated to ensuring a smooth transition while keeping you all informed and engaged throughout the process.
      • Process between CRM and Campaigns to ensure double opt-in contacts?

        I would like to ask for a few clarifications to ensure we fully comply with best practices and legal requirements: According to the documentation (Zoho Campaigns CRM sync – Default option), the best and recommended way to sync contacts is by using the
      • What's New in Zoho Inventory | August – October 2025

        Hello customers, The last quarter has been incredibly productive! We've released a powerful slate of new features and enhancements in Zoho Inventory designed to give you better control, greater efficiency, and expanded functionality across your inventory
      • Search not working!

        I have items in my notebook tagged but when I search for a tag nothing comes up! Any fix for this?
      • Let's Talk Recruit: Meet Zia, your all-in-one AI assistant (Part-2)

        Welcome back to Let’s Talk Recruit series. In Part 1, we introduced Zia and how AI is reshaping the way recruiters work. This time, we’re taking a closer look at how far Zia has come and how each update continues to simplify your everyday tasks. When
      • Zoho Developer - Feature Request Platform

        Zoho Developer is one of the most underatted platform in zoho ecosystem, however, it may just be what zoho needs to welcome more people to use Zoho Services. The more developers you have creating zoho creator applications and zoho extensions the more
      • Last/Previous month in relative date filter

        In the relative date filter, what is the difference between "Last 2 months" and "Previous 2 months"? So, if we are on 25-July, then is my understanding correct of the following: Last 2 months :=:      25-May~24 July Previous 2 months :=:     01-May~30-June Thanks.
      • Tags get removed from notes on mobile

        I don't know why this keeps happening even after all these app updates but if you log out and log back into the app or you reinstall it, all the tags you assigned to your notes get taken off, but if you go on the desktop version the tags are still there.
      • Show Zoom Link in Recipient's Calendar

        We set up meetings within a record, selecting the "Make this an online meeting".  We use Zoom. Most of the recipients go to their calendar (usually Gmail or Outlook, corporate) to join the Zoom meeting, but there is no Zoom link in the calendar. Can this
      • How to fetch custom fields for time entries in Zoho Project API v3

        In the previous Zoho Projects REST API, we were able to retrieve custom field details for time entries, including picklist options, using the endpoint: GET /restapi/portal/[PORTAL_ID]/timesheetcustomfields In the new Zoho Projects API v3, we tried using
      • VAT rates - exempt and out of scope

        Good Evening, UK based company here. I am a bit confused in respect of setting up VAT rates for exempt goods and services; at present I am simply leaving the VAT rate blank in the transactions in order to prevent any VAT appearing in the VAT return. When
      • Zoho is blocking emails I subscribe to from one sender

        About 4 months ago I stopped receiving newsletters that I subscribe to from @thedispatch.com. They tell me that zoho's server is blocking them. I've added them to my contacts list, but they're not even reaching my inbox. I don't know how to troubleshoot
      • Next Page