Took us a while to find this and perhaps others would have a quicker way but here's the instructions on getting the records from the performance module in Zoho People API.
Why?
Cos it took us a while. The online forums seem to go back over a decade and the documentation seems to have gaps; or simply modules are so custom/bespoke the documentation has started to genericize.
My client has seen the Goals or Objectives report under "Organization Reports" but would like some additional fields/columns added to it. Turns out, it's a system report you can't change. Enter Zoho Analytics... well almost, Analytics will sync with Zoho People but I wasn't able to select the performance modules (Client Review and Placement Tech Survey having been disabled)...
How?
So this might seem obvious but after trying to scan the meta data (snippets at the bottom of this article), the simplest way was right in front of us. We're going to use the interface to find the form where staff have been entering employees goals, then we're going to query it and push it to Zoho Analytics.
Find the form link name:
Putting this here as it should be what we first did! This is with reference to the Zoho People version 5:
- Login to Zoho People as an administrator (don't have to be super admin)
- Go to Settings (cog icon in the top right)
- Click on "Performance"
- Click on "Extend Service"
- Click on the form you want the data from, in our case "Objectives" (renamed from "Goals")
- Note the label name in the top right:
The Zoho People function to retrieve this data:
copyraw
This should give you all the fields that will be pulled from "Goals" and some records with data./* ******************************************************************************* Function: String fn_PushPerformanceToAnalytics(int p_RecordID) Label: Fn - Push Performance Data to Analytics Trigger: Workflow when an objective is created or edited. Purpose: Pushes the data and columns we need to Zoho Analytics to produce an Objectives report with the requested columns. Inputs: int p_RecordID Outputs: - Date Created: 2025-09-18 (Joel Lipman) - Initial release Date Modified: ??? - ??? ******************************************************************************* */ v_CountTotal = 0; m_OutputAll = Map(); v_Endpoint_Goals = "https://people.zoho.com/api/forms/P_Goals/getRecords"; r_GoalsData = invokeurl [ url :v_Endpoint_Goals type :GET connection:"people_cf" ]; m_GoalsResponse = ifnull(r_GoalsData.get("response"), Map()); l_GoalRecords = ifnull(m_GoalsResponse.get("result"), List()); for each m_GoalRecord in l_GoalRecords { v_CountTotal = v_CountTotal + 1; l_GoalKeys = m_GoalRecord.keys(); if(l_GoalKeys.size()>0) { v_GoalKey = l_GoalKeys.get(0); //info "Record ID: " + v_GoalKey; // m_ThisGoal = m_GoalRecord.get(v_GoalKey).get(0).toMap(); //info "Goal Record Data: " + m_ThisGoal; if(!isNull(m_ThisGoal.get("Zoho_ID"))) { m_OutputRecord = Map(); m_OutputRecord.put("Record Index", v_CountTotal); for each v_Field in m_ThisGoal.keys() { m_OutputRecord.put(v_Field, m_ThisGoal.get(v_Field)); //info "Field: " + v_Field + " || Value: " + m_ThisGoal.get(v_Field); } m_OutputAll.put(v_GoalKey, m_OutputRecord); } } // // for testing I'm doing 3 records at a time if(v_CountTotal > 3) { break; } } info m_OutputAll; return "";
- /* *******************************************************************************
- Function: String fn_PushPerformanceToAnalytics(int p_RecordID)
- Label: Fn - Push Performance Data to Analytics
- Trigger: Workflow when an objective is created or edited.
- Purpose: Pushes the data and columns we need to Zoho Analytics to produce an Objectives report with the requested columns.
- Inputs: int p_RecordID
- Outputs: -
- Date Created: 2025-09-18 (Joel Lipman)
- - Initial release
- Date Modified: ???
- - ???
- ******************************************************************************* */
- v_CountTotal = 0;
- m_OutputAll = Map();
- v_Endpoint_Goals = "https://people.zoho.com/api/forms/P_Goals/getRecords";
- r_GoalsData = invokeUrl
- [
- url :v_Endpoint_Goals
- type :GET
- connection:"people_cf"
- ];
- m_GoalsResponse = ifnull(r_GoalsData.get("response"), Map());
- l_GoalRecords = ifnull(m_GoalsResponse.get("result"), List());
- for each m_GoalRecord in l_GoalRecords
- {
- v_CountTotal = v_CountTotal + 1;
- l_GoalKeys = m_GoalRecord.keys();
- if(l_GoalKeys.size()>0)
- {
- v_GoalKey = l_GoalKeys.get(0);
- //info "Record ID: " + v_GoalKey;
- //
- m_ThisGoal = m_GoalRecord.get(v_GoalKey).get(0).toMap();
- //info "Goal Record Data: " + m_ThisGoal;
- if(!isNull(m_ThisGoal.get("Zoho_ID")))
- {
- m_OutputRecord = Map();
- m_OutputRecord.put("Record Index", v_CountTotal);
- for each v_Field in m_ThisGoal.keys()
- {
- m_OutputRecord.put(v_Field, m_ThisGoal.get(v_Field));
- //info "Field: " + v_Field + " || Value: " + m_ThisGoal.get(v_Field);
- }
- m_OutputAll.put(v_GoalKey, m_OutputRecord);
- }
- }
- //
- // for testing I'm doing 3 records at a time
- if(v_CountTotal > 3)
- {
- break;
- }
- }
- info m_OutputAll;
- return "";
Create the Zoho Analytics table
- Create a blank Excel file / spreadsheet / csv
- Copy the names of fields you want to store (I'm taking all of it), and paste them into the header of the spreadsheet with each one occupying one cell.
- Enter a description in the 2nd row to convince Analytics there is some data in the table we are going to import. You should end up with something like:
- I'm going to save this file as Zoho_Analytics_Performance_Goals.xlsx
- Login to Zoho Analytics
- Select "Data Sources" then Add New Data Source
- Select "Files" (has the icon of MS Excel on it)
- Give it a table name. I've gone with Performance Objectives / Goals
- File Type I said was Excel and then clicked on "Choose File" (with data location set to "Local Drive")
- Click on Next have a quick glance of the preview and then click on Next
- First row contains column names = Yes, leave everything else as plain text except for Description which you should change to a Multi Line Text.
- Click on Create. You should end up with something like
- Get the ZohoAnalytics Org ID:
- Login to ZohoAnalytics and ensure you are at the "Home" level
- Click on the cog icon in the top-right for "Settings"
- You should be in the "Organization Settings"
- Your Org ID is in the URL as the last number after the slash /org-details/###### (eg. "123456789")
- Note this down for later use
- Create a connection to Zoho Analytics:
- Above the function in Zoho People > Connections > Create Connection
- Select Zoho Analytics and give the required scopes (I've cheated and gone for ZohoAnalytics.fullaccess.all)
- Note the connection link name (mine is called "zanalytics")
The Zoho People function modified to pull the Goals data and push it into Zoho Analytics:
copyraw
/* ******************************************************************************* Function: String fn_PushPerformanceToAnalytics(int p_RecordID) Label: Fn - Push Performance Data to Analytics Trigger: Workflow when an objective is created or edited. Purpose: Pushes the data and columns we need to Zoho Analytics to produce an Objectives report with the requested columns. Inputs: int p_RecordID Outputs: - Date Created: 2025-09-18 (Joel Lipman) - Initial release Date Modified: ??? - ??? ******************************************************************************* */ // // init v_CountTotal = 0; // // the zoho analytics org ID noted earlier v_AnalyticsOrgID = "123456879"; // // query goals endpoint v_Endpoint_Goals = "https://people.zoho.com/api/forms/P_Goals/getRecords"; r_GoalsData = invokeurl [ url :v_Endpoint_Goals type :GET connection:"zpeople" ]; // // parse the response loop through each record returned m_GoalsResponse = ifnull(r_GoalsData.get("response"),Map()); l_GoalRecords = ifnull(m_GoalsResponse.get("result"),List()); for each m_GoalRecord in l_GoalRecords { // // start an increment for counting the records processed v_CountTotal = v_CountTotal + 1; // // for the response structure, we need to extrat the first key as the Zoho ID l_GoalKeys = m_GoalRecord.keys(); if(l_GoalKeys.size() > 0) { // // messy but this is the key to the record in the list - happens to be the Zoho ID as well v_GoalKey = l_GoalKeys.get(0); //info "Record ID: " + v_GoalKey; // m_ThisGoal = m_GoalRecord.get(v_GoalKey).get(0).toMap(); //info "Goal Record Data: " + m_ThisGoal; // if(!isNull(m_ThisGoal.get("Zoho_ID"))) { m_OutputRecord = Map(); // // obtain each field in this record and create our own map... // in this case, my table exactly matches the keys so this isn't really necessary but just in case I wanted to filter some fields out... for each v_Field in m_ThisGoal.keys() { m_OutputRecord.put(v_Field,m_ThisGoal.get(v_Field).toString()); //info "Field: " + v_Field + " || Value: " + m_ThisGoal.get(v_Field); } // // Add this row to Analytics m_Header = Map(); m_Header.put("ZANALYTICS-ORGID",v_AnalyticsOrgID); m_Columns = Map(); m_Columns.put("columns", m_OutputRecord); m_Params = Map(); m_Params.put("CONFIG", m_Columns.toString()); info m_OutputRecord; // // in analytics, browse to the target table and note the URL IDs after workspace and view v_WorkspaceID = "1234567000008912345"; v_TableID = "9876543000002198765"; v_Endpoint2 = "https://analyticsapi.zoho.com/restapi/v2/workspaces/"+v_WorkspaceID+"/views/"+v_TableID+"/rows"; info v_Endpoint2; // r_AddRow = invokeUrl [ url: v_Endpoint2 type: POST parameters: m_Params headers: m_Header connection: "zanalytics" ]; info r_AddRow; } } // // used while debugging / remove once in production if(v_CountTotal >= 3) { break; } } return "Processed " + v_CountTotal + " record(s)";
- /* *******************************************************************************
- Function: String fn_PushPerformanceToAnalytics(int p_RecordID)
- Label: Fn - Push Performance Data to Analytics
- Trigger: Workflow when an objective is created or edited.
- Purpose: Pushes the data and columns we need to Zoho Analytics to produce an Objectives report with the requested columns.
- Inputs: int p_RecordID
- Outputs: -
- Date Created: 2025-09-18 (Joel Lipman)
- - Initial release
- Date Modified: ???
- - ???
- ******************************************************************************* */
- //
- // init
- v_CountTotal = 0;
- //
- // the zoho analytics org ID noted earlier
- v_AnalyticsOrgID = "123456879";
- //
- // query goals endpoint
- v_Endpoint_Goals = "https://people.zoho.com/api/forms/P_Goals/getRecords";
- r_GoalsData = invokeUrl
- [
- url :v_Endpoint_Goals
- type :GET
- connection:"zpeople"
- ];
- //
- // parse the response loop through each record returned
- m_GoalsResponse = ifnull(r_GoalsData.get("response"),Map());
- l_GoalRecords = ifnull(m_GoalsResponse.get("result"),List());
- for each m_GoalRecord in l_GoalRecords
- {
- //
- // start an increment for counting the records processed
- v_CountTotal = v_CountTotal + 1;
- //
- // for the response structure, we need to extrat the first key as the Zoho ID
- l_GoalKeys = m_GoalRecord.keys();
- if(l_GoalKeys.size() > 0)
- {
- //
- // messy but this is the key to the record in the list - happens to be the Zoho ID as well
- v_GoalKey = l_GoalKeys.get(0);
- //info "Record ID: " + v_GoalKey;
- //
- m_ThisGoal = m_GoalRecord.get(v_GoalKey).get(0).toMap();
- //info "Goal Record Data: " + m_ThisGoal;
- //
- if(!isNull(m_ThisGoal.get("Zoho_ID")))
- {
- m_OutputRecord = Map();
- //
- // obtain each field in this record and create our own map...
- // in this case, my table exactly matches the keys so this isn't really necessary but just in case I wanted to filter some fields out...
- for each v_Field in m_ThisGoal.keys()
- {
- m_OutputRecord.put(v_Field,m_ThisGoal.get(v_Field).toString());
- //info "Field: " + v_Field + " || Value: " + m_ThisGoal.get(v_Field);
- }
- //
- // Add this row to Analytics
- m_Header = Map();
- m_Header.put("ZANALYTICS-ORGID",v_AnalyticsOrgID);
- m_Columns = Map();
- m_Columns.put("columns", m_OutputRecord);
- m_Params = Map();
- m_Params.put("CONFIG", m_Columns.toString());
- info m_OutputRecord;
- //
- // in analytics, browse to the target table and note the URL IDs after workspace and view
- v_WorkspaceID = "1234567000008912345";
- v_TableID = "9876543000002198765";
- v_Endpoint2 = "https://analyticsapi.zoho.com/restapi/v2/workspaces/"+v_WorkspaceID+"/views/"+v_TableID+"/rows";
- info v_Endpoint2;
- //
- r_AddRow = invokeUrl
- [
- url: v_Endpoint2
- type: POST
- parameters: m_Params
- headers: m_Header
- connection: "zanalytics"
- ];
- info r_AddRow;
- }
- }
- //
- // used while debugging / remove once in production
- if(v_CountTotal >= 3)
- {
- break;
- }
- }
- return "Processed " + v_CountTotal + " record(s)";
Importante Note: The above has a caveat in that it pushes the data from Zoho People to Zoho Analytics. It doesn't check to see if the data is already there, it doesn't update, it just keeps adding rows. Modify the function above (the one hosted in ZohoPeople) to account for this. It's the end of the day and I will do that tomorrow but there is only so much fun one can have in a day...
Error(s) Encountered
- {"status":"failure","summary":"LESS_THAN_MIN_OCCURANCE","data":{"errorCode":8504,"errorMessage":"The parameter CONFIG is not proper(Has not been sent or is less than required count)"}} Crazy! sometimes the invokeUrl wants its parameters changed using .toString()... sometimes it doesn't, as it did in this case.
Additional / Optional stuff to do but please don't waste your time.
Out of sheer boredom: fn_GetMetaData_FormsFields
copyraw
v_NewLine = hexToText("0A"); l_CsvRows = List(); v_FormsEndpoint = "https://people.zoho.com/api/forms"; r_Forms = invokeurl [ url :v_FormsEndpoint type :GET connection:"my_people_connection" ]; m_Response = ifnull(r_Forms.get("response"),Map()); l_Forms = ifnull(m_Response.get("result"),List()); for each m_Form in l_Forms { if(!isNull(m_Form.get("formLinkName"))) { v_FormName = m_Form.get("displayName"); v_FieldsEndpoint = "https://people.zoho.com/people/api/forms/" + m_Form.get("formLinkName") + "/components"; r_Fields = invokeurl [ url :v_FieldsEndpoint type :GET connection:"my_people_connection" ]; m_FieldsResponse = ifnull(r_Fields.get("response"),Map()); l_Fields = ifnull(m_FieldsResponse.get("result"),List()); for each m_Field in l_Fields { if(!isNull(m_Field.get("comptype"))) { l_CsvRow = List(); l_CsvRow.add(v_FormName); l_CsvRow.add(m_Form.get("formLinkName")); l_CsvRow.add(m_Field.get("displayname")); l_CsvRow.add(m_Field.get("labelname")); l_CsvRow.add(m_Field.get("comptype")); l_CsvRows.add(l_CsvRow.toString()); } } } } v_Filename = "My_Form_Fields_MetaData.csv"; f_MyCSV = l_CsvRows.toString(v_NewLine).toFile(v_Filename); f_MyCSV.setParamName("attachment"); sendmail [ from :zoho.adminuserid to :"<my_email>" subject :"My Form Fields" message :"See attached" Attachments :file:f_MyCSV ] return "";
- v_NewLine = hexToText("0A");
- l_CsvRows = List();
- v_FormsEndpoint = "https://people.zoho.com/api/forms";
- r_Forms = invokeUrl
- [
- url :v_FormsEndpoint
- type :GET
- connection:"my_people_connection"
- ];
- m_Response = ifnull(r_Forms.get("response"),Map());
- l_Forms = ifnull(m_Response.get("result"),List());
- for each m_Form in l_Forms
- {
- if(!isNull(m_Form.get("formLinkName")))
- {
- v_FormName = m_Form.get("displayName");
- v_FieldsEndpoint = "https://people.zoho.com/people/api/forms/" + m_Form.get("formLinkName") + "/components";
- r_Fields = invokeUrl
- [
- url :v_FieldsEndpoint
- type :GET
- connection:"my_people_connection"
- ];
- m_FieldsResponse = ifnull(r_Fields.get("response"),Map());
- l_Fields = ifnull(m_FieldsResponse.get("result"),List());
- for each m_Field in l_Fields
- {
- if(!isNull(m_Field.get("comptype")))
- {
- l_CsvRow = List();
- l_CsvRow.add(v_FormName);
- l_CsvRow.add(m_Form.get("formLinkName"));
- l_CsvRow.add(m_Field.get("displayname"));
- l_CsvRow.add(m_Field.get("labelname"));
- l_CsvRow.add(m_Field.get("comptype"));
- l_CsvRows.add(l_CsvRow.toString());
- }
- }
- }
- }
- v_Filename = "My_Form_Fields_MetaData.csv";
- f_MyCSV = l_CsvRows.toString(v_NewLine).toFile(v_Filename);
- f_MyCSV.setParamName("attachment");
- sendmail
- [
- from :zoho.adminuserid
- to :"<my_email>"
- subject :"My Form Fields"
- message :"See attached"
- Attachments :file:f_MyCSV
- ]
- return "";
Out of sheer boredom: fn_GetMetaData_ViewsColumns
copyraw
v_NewLine = hexToText("0A"); l_CsvRows = List(); v_FormsEndpoint = "https://people.zoho.com/api/views"; r_Forms = invokeurl [ url :v_FormsEndpoint type :GET connection:"my_people_connection" ]; m_Response = ifnull(r_Forms.get("response"),Map()); l_Forms = ifnull(m_Response.get("result"),List()); for each m_Form in l_Forms { l_CsvRows.add(m_Form.keys()); } v_Filename = "My_Form_Fields_ViewColumns.csv"; f_MyCSV = l_CsvRows.toString(v_NewLine).toFile(v_Filename); f_MyCSV.setParamName("attachment"); sendmail [ from :zoho.adminuserid to :"<my_email>" subject :"My View Columns" message :"See attached" Attachments :file:f_MyCSV ] return "";
- v_NewLine = hexToText("0A");
- l_CsvRows = List();
- v_FormsEndpoint = "https://people.zoho.com/api/views";
- r_Forms = invokeUrl
- [
- url :v_FormsEndpoint
- type :GET
- connection:"my_people_connection"
- ];
- m_Response = ifnull(r_Forms.get("response"),Map());
- l_Forms = ifnull(m_Response.get("result"),List());
- for each m_Form in l_Forms
- {
- l_CsvRows.add(m_Form.keys());
- }
- v_Filename = "My_Form_Fields_ViewColumns.csv";
- f_MyCSV = l_CsvRows.toString(v_NewLine).toFile(v_Filename);
- f_MyCSV.setParamName("attachment");
- sendmail
- [
- from :zoho.adminuserid
- to :"<my_email>"
- subject :"My View Columns"
- message :"See attached"
- Attachments :file:f_MyCSV
- ]
- return "";
Category: Zoho :: Article: 909
Add comment