Process ownership
The user making the API call becomes the process owner automatically, but won’t be assigned to individual tasks unless explicitly included in the assignee list.
Normally in Tallyfy, you’d create a template and then launch it. But sometimes you need an ad-hoc project — just a one-time set of tasks you want to track like a regular process.
Tallyfy lets you launch an “empty process” with no predefined steps. You can then add one-off tasks to it and track everything together. Think of it as running simple one-off projects, not just repeatable processes from a template.
The one-off task creation endpoint (POST /organizations/{org_id}/tasks) accepts a separate_task_for_each_assignee parameter. When you set it to true, the system:
POST https://go.tallyfy.com/api/organizations/{org_id}/tasks| Header | Value | Required |
|---|---|---|
Authorization | Bearer {access_token} | Yes |
X-Tallyfy-Client | APIClient | Yes |
Accept | application/json | Yes |
Content-Type | application/json | Yes |
{ "title": "Task title", "summary": "Optional task description", "owners": { "users": ["userId1", "userId2", "userId3"], "guests": ["guest@example.com"], "groups": ["groupId1"] }, "separate_task_for_each_assignee": true, "task_type": "task", "deadline": "YYYY-MM-DD 17:00:00", "everyone_must_complete": false, "is_soft_start_date": true, "started_at": "YYYY-MM-DD 09:00:00", "prevent_guest_comment": false, "can_complete_only_assignees": false, "max_assignable": 0, "webhook": "https://your-webhook.com/endpoint", "tags": ["tagId1", "tagId2"], "top_secret": false, "form_fields": [ { "label": "Project Name", "field_type": "text", "required": true, "guidance": "Enter the project name", "position": 1 } ]}| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Title for the process and tasks (max 600 characters) |
task_type | string | Yes | One of: task, approval, expiring, email, expiring_email |
owners | object | Yes | Contains arrays of users, guests, and/or groups |
deadline | string | Yes | Deadline in YYYY-MM-DD HH:MM:SS format |
separate_task_for_each_assignee | boolean | No | Set to true to create an empty process container. When omitted or false, a standalone one-off task is created instead. |
summary | string | No | Description or instructions for the tasks |
everyone_must_complete | boolean | No | Whether all assignees must complete the task (default: false) |
is_soft_start_date | boolean | No | Whether the start date is flexible (default: false) |
started_at | string | No | Start date in YYYY-MM-DD HH:MM:SS format (defaults to current time) |
prevent_guest_comment | boolean | No | Block guests from commenting (default: false) |
can_complete_only_assignees | boolean | No | Only assignees can complete the task (default: false) |
max_assignable | integer | No | Maximum assignees allowed (default: 0 = unlimited) |
webhook | string | No | URL for webhook notifications on task events |
tags | array | No | Array of tag IDs (32-character strings) |
top_secret | boolean | No | Mark task as confidential (default: false) |
form_fields | array | No | Array of form field definitions for data collection |
Each object in form_fields supports:
| Field Property | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Display label for the field |
field_type | string | Yes | One of: text, textarea, radio, dropdown, multiselect, number, email, url, date, time, datetime, checkbox, file |
required | boolean | Yes | Whether the field is mandatory |
options | array | Conditional | Required for radio, dropdown, and multiselect types |
guidance | string | No | Help text for the field |
position | integer | No | Display order |
Things to know:
async function createProcessWithoutTemplate(orgId, accessToken, taskData) { const response = await fetch( `https://go.tallyfy.com/api/organizations/${orgId}/tasks`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'X-Tallyfy-Client': 'APIClient', 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: taskData.title, summary: taskData.description || '', owners: { users: taskData.userIds || [] }, separate_task_for_each_assignee: true, task_type: 'task', deadline: taskData.deadline || getDefaultDeadline(), everyone_must_complete: false, is_soft_start_date: true }) } );
if (!response.ok) { throw new Error(`Failed to create process: ${response.status}`); }
const result = await response.json();
// The run (process) info is included when separate_task_for_each_assignee is true const processId = result.data?.run?.id; console.log(`Created process: ${processId}`); return result;}
function getDefaultDeadline() { const date = new Date(); date.setDate(date.getDate() + 7); return date.toISOString().slice(0, 19).replace('T', ' ');}import requestsfrom datetime import datetime, timedeltafrom typing import List, Dict, Optional
class TallyfyProcessCreator: def __init__(self, org_id: str, access_token: str): self.org_id = org_id self.access_token = access_token self.api_base = "https://go.tallyfy.com/api"
def create_process_without_template( self, title: str, user_ids: List[str], description: str = "", deadline: Optional[datetime] = None ) -> Dict: if deadline is None: deadline = datetime.now() + timedelta(days=7)
payload = { "title": title, "summary": description, "owners": {"users": user_ids}, "separate_task_for_each_assignee": True, "task_type": "task", "deadline": deadline.strftime("%Y-%m-%d %H:%M:%S"), "everyone_must_complete": False, "is_soft_start_date": True }
response = requests.post( f"{self.api_base}/organizations/{self.org_id}/tasks", headers={ "Authorization": f"Bearer {self.access_token}", "X-Tallyfy-Client": "APIClient", "Accept": "application/json", "Content-Type": "application/json" }, json=payload ) response.raise_for_status() result = response.json()
if "data" in result and "run" in result["data"]: process_info = result["data"]["run"] print(f"Created process {process_info['id']}")
return result
# Usagecreator = TallyfyProcessCreator("your-org-id", "your-access-token")result = creator.create_process_without_template( title="Review Q4 Financial Report", user_ids=["user_123", "user_456", "user_789"], description="Review and provide feedback on the Q4 financial report", deadline=datetime.now() + timedelta(days=7))<?php
class TallyfyProcessCreator { private $orgId; private $accessToken; private $apiBase = 'https://go.tallyfy.com/api';
public function __construct($orgId, $accessToken) { $this->orgId = $orgId; $this->accessToken = $accessToken; }
public function createProcessWithoutTemplate($title, $userIds, $description = '', $deadline = null) { if ($deadline === null) { $deadline = date('Y-m-d H:i:s', strtotime('+7 days')); }
$payload = [ 'title' => $title, 'summary' => $description, 'owners' => ['users' => $userIds], 'separate_task_for_each_assignee' => true, 'task_type' => 'task', 'deadline' => $deadline, 'everyone_must_complete' => false, 'is_soft_start_date' => true ];
$ch = curl_init("{$this->apiBase}/organizations/{$this->orgId}/tasks"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $this->accessToken, 'X-Tallyfy-Client: APIClient', 'Accept: application/json', 'Content-Type: application/json' ]);
$response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($httpCode !== 200 && $httpCode !== 201) { throw new Exception("Failed to create process: HTTP $httpCode"); }
return json_decode($response, true); }}
// Usage$creator = new TallyfyProcessCreator('your-org-id', 'your-access-token');$result = $creator->createProcessWithoutTemplate( 'Complete Compliance Checklist', ['user_123', 'user_456'], 'Complete your assigned compliance checklist items', date('Y-m-d H:i:s', strtotime('+14 days')));require 'net/http'require 'json'require 'uri'
class TallyfyProcessCreator def initialize(org_id, access_token) @org_id = org_id @access_token = access_token @api_base = 'https://go.tallyfy.com/api' end
def create_process_without_template(title, user_ids, description = '', deadline = nil) deadline ||= (Time.now + 7 * 24 * 60 * 60).strftime('%Y-%m-%d %H:%M:%S')
payload = { title: title, summary: description, owners: { users: user_ids }, separate_task_for_each_assignee: true, task_type: 'task', deadline: deadline, everyone_must_complete: false, is_soft_start_date: true }
uri = URI("#{@api_base}/organizations/#{@org_id}/tasks") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true
request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{@access_token}" request['X-Tallyfy-Client'] = 'APIClient' request['Accept'] = 'application/json' request['Content-Type'] = 'application/json' request.body = payload.to_json
response = http.request(request)
unless response.code.to_i.between?(200, 201) raise "Failed to create process: HTTP #{response.code}" end
JSON.parse(response.body) endend
# Usagecreator = TallyfyProcessCreator.new('your-org-id', 'your-access-token')result = creator.create_process_without_template( 'Complete Security Training', ['user_123', 'user_456', 'user_789'], 'Complete the mandatory security training module', (Time.now + 14 * 24 * 60 * 60).strftime('%Y-%m-%d %H:%M:%S'))If your process needs different tasks with unique titles, use a two-step approach:
separate_task_for_each_assignee: true to establish the processrun_id from step 1async function createMultiStageProcess(orgId, accessToken, tasks) { // Step 1: Create process with the first task const firstTask = tasks[0]; const processResponse = await fetch( `https://go.tallyfy.com/api/organizations/${orgId}/tasks`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'X-Tallyfy-Client': 'APIClient', 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: firstTask.title, summary: firstTask.description, owners: { users: firstTask.userIds }, separate_task_for_each_assignee: true, task_type: 'task', deadline: firstTask.deadline }) } );
const processResult = await processResponse.json(); const processId = processResult.data?.run?.id;
if (!processId) { throw new Error('Failed to create process'); }
// Step 2: Add remaining tasks to the process for (let i = 1; i < tasks.length; i++) { const task = tasks[i]; await fetch( `https://go.tallyfy.com/api/organizations/${orgId}/tasks`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'X-Tallyfy-Client': 'APIClient', 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ title: task.title, summary: task.description, owners: { users: task.userIds }, run_id: processId, task_type: 'task', deadline: task.deadline, position: i + 1 }) } ); }
return processId;}def create_multi_stage_process(self, tasks: List[Dict]) -> str: if not tasks: raise ValueError("No tasks provided")
# Step 1: Create process with first task first_task = tasks[0] process_result = self.create_process_without_template( title=first_task['title'], user_ids=first_task['userIds'], description=first_task.get('description', ''), deadline=first_task.get('deadline') )
process_id = process_result['data']['run']['id']
# Step 2: Add remaining tasks for i, task in enumerate(tasks[1:], start=2): payload = { "title": task['title'], "summary": task.get('description', ''), "owners": {"users": task['userIds']}, "run_id": process_id, "task_type": "task", "deadline": task.get('deadline', self._get_default_deadline()), "position": i }
response = requests.post( f"{self.api_base}/organizations/{self.org_id}/tasks", headers={ "Authorization": f"Bearer {self.access_token}", "X-Tallyfy-Client": "APIClient", "Accept": "application/json", "Content-Type": "application/json" }, json=payload ) response.raise_for_status()
return process_idThe API returns the first created task. When separate_task_for_each_assignee is true, the response includes the linked process (run) data:
{ "data": { "id": "task_id", "title": "Task title", "status": "not-started", "deadline": "YYYY-MM-DD 17:00:00", "run": { "id": "process_id", "name": "Task title", "started_at": "YYYY-MM-DD 09:00:00" } }}To see all created tasks, query the process using the returned run.id.
Process ownership
The user making the API call becomes the process owner automatically, but won’t be assigned to individual tasks unless explicitly included in the assignee list.
Task independence
With everyone_must_complete set to false (the default), each assignee can complete their task independently.
MISC system template
Behind the scenes, Tallyfy uses a special “MISC” system template that exists in every organization. You don’t need to configure anything — it’s handled automatically.
Rate limiting
The API allows 600 requests per minute. When creating processes in bulk, keep this limit in mind and add delays between requests for large batches.
try { const result = await createProcessWithoutTemplate(orgId, token, taskData); console.log(`Process created: ${result.data.run.id}`);} catch (error) { if (error.status === 401) { // Token expired -- refresh and retry await refreshToken(); return retry(); } else if (error.status === 422) { // Validation error (missing required fields, invalid task_type, etc.) console.error('Invalid task data:', error.response.errors); } else if (error.status === 429) { // Rate limited -- wait and retry await sleep(60000); return retry(); } else { console.error('Unexpected error:', error); throw error; }}summary — give assignees enough information to act on the taskrun.id — you’ll need it to track progress and add more tasks laterPostman > Working with templates and processes