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

    • Automation Series: Auto-create Dependent Task on Status Change

      In Zoho Projects, you can automatically create and assign a dependent task when a task’s status is updated. This helps teams stay aligned, ensures reviews happen on time, and reduces manual effort. In this post, we’ll walk through an easy setup using
    • 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
    • Recent Topics

    • Evernote (ENEX) import limitations

      I have been with Evernote since 2010, but the latest price increase is ridiculous. I am currently testing Zoho Notebook as a replacement. I am impressed so far - if it were not for critical need to import legacy Evernote notes, I would 100% migrate to
    • Cannot see Application from Lookup field

      Hi all, I am trying to access data for an application on our account via a lookup field; however, the application doesn't appear in the dropdown at all. Can anyone shed any light on this, please? I have asked Zoho support; however, they're just as confused,
    • Zoho CRM Meetings Module Issues

      We have a use-case that is very common in today's world, but won't work in Zoho CRM. We have an SDR (Sales Development Rep) who makes many calls per day to Leads and Contacts, and schedules meetings for our primary Sales Reps. He does this by logging
    • Zoho Books integration sync from Zoho CRM does not work

      Hi Zoho Community & Zoho Support We just tried to get a sync some products into Zoho Books from CRM using the native sync and we're getting an error: "It looks like some mandatory fields you're trying to map are empty. Please provide valid field names
    • Disable Auto Send Email Notification in Invoice Creation

      Here is what I am facing Make payment via Razor Pay (All Good) Capture payload data on my webapp Send this data, along with other data to Zoho books to create the invoice However in this step - An email goes to the client + The Invoice remains as draft
    • P & L Sub-categorized accounts

      How can I show sub-categorized Income and Expense accounts on the P & L report?
    • Report showing Bill Details with Project and Sales Invoice Number

      Hi There, I am hoping that someone can help, I am looking for report that can show the bill and expense details along with project its as assigned to and the invoice number that the sales has been raised in. The goal is I can filter a customer/project
    • Advanced Payment for Inventory Items with serial numbers

      Hello, We sell equipment that we track the unique serial numbers on using Sales Orders. We can charge the customers an advanced payment, then the balance on delivery. We cannot figure out a way to do this in Books/Inventory: - Cannot part invoice a SO
    • Is it possible to restrict ZCRM user to see only custom views created by administrator

      I have segmented data in my CRM and I want to allow different users to be able to see only parts of it based on some criteria. I've tried to create and share a custom view, but then there is always an option for user to see all open lead for example.
    • Issues Logging into ZOHO

      Hello, one of my coworkers is having issues logging into ZOHO, she has requested a code when entering and the email is correct but she has not received the code. can you help us with this?
    • Google Fonts Integration in Pagesense Popup Editor

      Hello Zoho Pagesense Team, We hope you're doing well. We’d like to submit a feature request to enhance Zoho Pagesense’s popup editor with Google Fonts support. Current Limitation: Currently, Pagesense offers a limited set of default fonts. Google Fonts
    • Add Popup Rejection Metrics to Reports

      Hello Zoho PageSense Team, We would like to request improved reporting for popup interactions. Current Limitation: PageSense currently provides conversion data, but there is no clear visibility into: Popup rejections Popup closes (✕ button clicks) Dismissals
    • Ability to Reset / Reinitialize Popup Cookies

      Hello Zoho PageSense Team, We would like to request the ability to manually reset popup cookies. Current Limitation: At the moment, it is not possible to initiate a new popup cookie from the our side. Visitors who rejected or closed a popup will not see
    • Control Popup Cookie Expiration Duration

      Hello Zoho PageSense Team, We would like to request an enhancement related to popup cookie management. Current Limitation: Currently, PageSense popup cookies remain active for 365 days, and this duration cannot be modified by us. If a visitor closes or
    • Clone / Export Popup Design Across PageSense Projects

      Hello Zoho PageSense Team, We hope you’re doing well. We would like to request an enhancement that allows popup designs to be reused across different PageSense projects. Problem Statement: Currently, Zoho PageSense allows popups to be duplicated only
    • Are there settings for hyperlinks?

      Clicking a hyperlinked cell in Sheet creates this little pop-up with the actual hyperlink inside. Is it possible to have a 1-click link where if you click the cell it opens the link directly with no pop-up?
    • Automatically include all ticket attachments in the ticket resolution email

      Hello Zoho Community, We are implementing Zoho Desk in a real customer-facing production environment and have run into a limitation that is becoming a blocking requirement for our clients. The problem When a ticket is closed or resolved, Zoho Desk sends
    • Keep Zoho People Feature Requests in the Zoho People Forum

      Hello Zoho People Product Team, Greetings. We would like to submit a feature request regarding the handling of feature requests themselves, specifically for Zoho People. Issue: Feature Requests Being Moved to Zoho One Zoho People feature requests are
    • Can I write a check in Zoho Books with no associated bill?

      This currently does not seem possible, and I have a client that desperately needs this function if I am able to convert them with Quickbooks. Thank you in advance for your reply. 
    • Finding text within a ticket: Expand All or Search this Ticket

      The auto-collapse feature within a ticket is nice for screen scrolling, however it makes it difficult to find text within the ticket if the email is collapsed. In fact you cannot find text if it is collapsed. I would like to propose a feature that allows
    • Books & Desk. Client mapping

      Hi, I’ve been using Zoho Books for several years and am now looking to improve my customer service. I'm experimenting with Zoho Desk and want to sync and map my client data from Zoho Books. However, it seems that mapping requires both contacts to have
    • String handling

      If I cut a currency string from a quote and try and paste it into the Deal "Amount", it will fail unless I manually delete any commas. Dollar signs are no problem, but comma's seem to fail. Please correct this Input Validation error.
    • Allow Stripe Credit Card and Stripe ACH payment methods to be enabled separately on an invoice.

      I need to be able to pick at the invoice level whether Stripe Credit Card and/or Stripe ACH payment methods are available. Currently, I'm not able to select from the two Stripe payment methods individually on an invoice. However, there are some larger
    • Looking For Recruit Developer

      Hi everyone, I am looking for a Zoho Certified Developer to assist with a development project for MetalXpert. We are building a software system designed to bridge the gap between a candidate mobile app and an employer web portal using Zoho Recruit as
    • SKUs for Invoices, POs, etc.

      It doesn't appear that one can enable SKU display on invoices, POs, etc. This is problematic, and I don't see a good reason why this shouldn't be an option. Some of our vendors and customers use this in their system. Every other identifier code is available
    • Feature Request - Allow Customers To Pick Meeting Duration

      Hi Bookings Team, It would be great if there was an option to allow customers to pick a duration based on a max and minimum amount of time defined by me and in increments defined by me. For example, I have some slots which are available for customers
    • YouTube Live streaming? how to? Zoom has this feature, built-in. Can't find it on zoho meetings.

      YouTube Live streaming? how to? Zoom has this feature, built-in. Can't find it on zoho meetings.
    • Feature Request - A Way To Search Item Groups

      Hi Inventory Team, I can't find any way to filter or search by fields of Item Groups. It would be great to see that functionality added. I have a use case where a single product might come from 5 or more suppliers and each supplier's item is an Item in
    • Feature Reqeust - Include MPN In Selectable FIelds

      I have noticed that the MPN is not available to show in the list view of Items. Please consider adding it as EAN, UPC and ISBN are all available, so it doesn't make much sense to exclude this similar option. Thanks for considering my feedback.
    • Feature Request - Option To Hide Default System Fields on Items

      Hi Zoho Inventory Team, As far as I know it is not possible to hid some of the defult system fields on Items, such as UPC, MPN, EAN, ISBN. A good use case is that in many cases ISBN is not relevant and it would be an improved user experience if we could
    • Campaigns does not work!

      I am running into so many problems trying to use Zoho Campaigns, that I am seriously considering dropping the app from my (shrinking) list of Zoho applications I actually use. Apart from having to fight the software trying to create a design and email,
    • Feature Request - Make Available "Alias Name" Field In Item List View

      Hi Zoho Inventory Team, I have noticed that the "Alias Name" field does not appear on the list of selectable columns in the Customise Columns feature in the Items module. This would be very useful to see for businesses who are using the Alias Name field
    • product name field issue-n8n

      Hey guys, I have a question. I want to create a new product using the workflow. The problem is with the product name field; I don't know how to fill it in. The workflow starts with retrieving information from the leads table, retrieving links to scrape
    • How to filter Packages in zoho inventory api

      Hi Team, I want to perform some tasks in a schedular on the packages which are in "Shipped" state. I tried to use filter_by in my api call but in return I get response as {"code":-1,"message":"Given filter is not configured"} My Api request is as follows
    • CRM

      Is anyone else experiencing this issue? Our company is not moving out of using Gmail's web app. It just has more features and is a better email program than Zoho Mail. Gmail has an extension (Zoho CRM for Gmail) that we're using but we've found some serious
    • Syncing with Google calendar, Tasks and Events

      Is it possible to sync Zoho CRM calendar, task and events with Google Calendar's tasks and events. With the increasing adoption by many major tool suppliers to sync seamlessly with Google's offerings (for instance I use the excellent Any.do task planning
    • How can i view "Child" Accounts?

      It can be very useful in our field of business to know the parent-child account relationship. However, there seems to be a shortcoming in the parent account view: no child account list. How can we view the child accounts per each account?
    • Ability to assign Invoice Ownership through Deluge in FSM

      Hi, As part of our process, when a service appointment is completed, we automated the creation of the invoice based on a specific business logic using Deluge. When we do that, the "Owner" of the invoice in Zoho FSM is defaulted to the SuperAdmin. This
    • Easily perform calculations using dates with the new DATEDIF function

      Hey Zoho Writer users! We've enhanced Zoho Writer's formula capabilities with the new DATEDIF function. This allows you to calculate the difference between dates in days, months, and years. Function syntax: =DATEDIF(start_date, end_date, unit) Inputs:
    • Adding Comments Using Workflows - How to Change User Attributed

      We have worklflows in Desk where a comment is added to a ticket based on certain criteria. It seems that the comment added is always attributed to the user who last edited the workflow. This does not make sense for us because: - It's misleading to other
    • Next Page